;
-}
-
--repackageclasses
--allowaccessmodification
--overloadaggressively
-
-# Gson uses generic type information stored in a class file when working with fields. Proguard
-# removes such information by default, so configure it to keep all of it.
--keepattributes Signature,InnerClasses,EnclosingMethod
-
--dontwarn org.jetbrains.annotations.NotNull
--dontwarn org.jetbrains.annotations.Nullable
--dontwarn org.bouncycastle.jsse.BCSSLParameters
--dontwarn org.bouncycastle.jsse.BCSSLSocket
--dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
--dontwarn org.conscrypt.Conscrypt*
--dontwarn org.conscrypt.ConscryptHostnameVerifier
--dontwarn org.openjsse.javax.net.ssl.SSLParameters
--dontwarn org.openjsse.javax.net.ssl.SSLSocket
--dontwarn org.openjsse.net.ssl.OpenJSSE
-
--keepclassmembers class * implements android.os.Parcelable {
- public static final ** CREATOR;
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
deleted file mode 100644
index c4de29277..000000000
--- a/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/assets/webview/colors_dark.css b/app/src/main/assets/webview/colors_dark.css
deleted file mode 100644
index abd4e73a9..000000000
--- a/app/src/main/assets/webview/colors_dark.css
+++ /dev/null
@@ -1,156 +0,0 @@
-/* Primer Colors */
-/* Please also update colors_light.css with light mode appropriate colors when modifying this file. */
-
-:root {
- --blue-900: #082a52;
- --blue-800: #063366;
- --blue-700: #0b498f;
- --blue-600: #0c65c9;
- --blue-500: #0d6edb;
- --blue-400: #2e8fff;
- --blue-300: #85beff;
- --blue-200: #d1e6ff;
- --blue-100: #e5f2ff;
- --blue-000: #f5faff;
- --gray-1000: #050505;
- --gray-950: #0b0b0d;
- --gray-900: #17181a;
- --gray-850: #242528;
- --gray-800: #2e2f37;
- --gray-750: #383a42;
- --gray-700: #41434e;
- --gray-650: #4b4d58;
- --gray-600: #525560;
- --gray-550: #5e616e;
- --gray-500: #6c6f7e;
- --gray-450: #787c8c;
- --gray-400: #9194a1;
- --gray-350: #a9abb6;
- --gray-300: #bfc1c9;
- --gray-250: #d6d7dc;
- --gray-200: #e3e4e8;
- --gray-150: #eff0f5;
- --gray-100: #f7f7f9;
- --gray-050: #fbfbfc;
- --gray-000: #ffffff;
- --green-900: #184d25;
- --green-800: #1b612b;
- --green-700: #1e7533;
- --green-600: #2b8f43;
- --green-500: #32b24f;
- --green-400: #40d663;
- --green-300: #95f0ab;
- --green-200: #cbf7d5;
- --green-100: #e5ffeb;
- --green-000: #f5fff8;
- --yellow-900: #755f13;
- --yellow-800: #b89007;
- --yellow-700: #e0b112;
- --yellow-600: #ffcb1a;
- --yellow-500: #ffd74d;
- --yellow-400: #ffe166;
- --yellow-300: #ffec8a;
- --yellow-200: #fff6b8;
- --yellow-100: #fffce5;
- --yellow-000: #fffef5;
- --orange-900: #a84603;
- --orange-800: #c75204;
- --orange-700: #d65c09;
- --orange-600: #eb680e;
- --orange-500: #fa6f0f;
- --orange-400: #ff8a38;
- --orange-300: #ffae75;
- --orange-200: #ffd5b2;
- --orange-100: #ffeee0;
- --orange-000: #fffaf5;
- --red-900: #8f1d22;
- --red-800: #a82229;
- --red-700: #bd222d;
- --red-600: #d62b38;
- --red-500: #e04352;
- --red-400: #f55363;
- --red-300: #ff808d;
- --red-200: #ffb2bb;
- --red-100: #ffe0e4;
- --red-000: #fff0f2;
- --pink-900: #702653;
- --pink-800: #9e3674;
- --pink-700: #c2428e;
- --pink-600: #d63c99;
- --pink-500: #f051b0;
- --pink-400: #f576c2;
- --pink-300: #fa9bd4;
- --pink-200: #ffbde4;
- --pink-100: #fee0f2;
- --pink-000: #fff0f9;
- --purple-900: #2e1757;
- --purple-800: #3f2175;
- --purple-700: #522e8f;
- --purple-600: #6139a8;
- --purple-500: #7548c7;
- --purple-400: #916bd6;
- --purple-300: #b899f0;
- --purple-200: #dac7ff;
- --purple-100: #eae0ff;
- --purple-000: #f6f2ff;
- --textPrimary: var(--gray-050);
- --textSecondary: var(--gray-300);
- --textTertiary: var(--gray-400);
- --textPlaceholder: rgba(145, 148, 161, 0.5);
- --link: var(--blue-400);
- --appBackground: var(--gray-1000);
- --backgroundSecondary: var(--gray-900);
- --backgroundTertiary: var(--gray-850);
- --border: rgba(191, 193, 201, 0.16);
- --borderOpaque: var(--gray-700);
- --iconPrimary: var(--gray-300);
- --iconSecondary: var(--gray-500);
- --inputBackground: rgba(191, 193, 201, 0.12);
- --backgroundPrimary: var(--gray-1000);
- --backgroundElevatedPrimary: var(--gray-900);
- --backgroundElevatedSecondary: rgba(191, 193, 201, 0.04);
- --backgroundElevatedTertiary: rgba(191, 193, 201, 0.08);
- --backgroundInset: var(--gray-900);
- --link-hover: var(--gray-800);
- --color-icon-success: var(--green-600);
- --color-text-danger: var(--red-600);
- --diffLineNumberAdditionBackground: #08260f;
- --diffLineNumberAdditionText: #95f0ab;
- --diffLineAdditionBackground: #061c0b;
- --diffLineNumberDeletionBackground: #3b0507;
- --diffLineNumberDeletionText: #ff808d;
- --diffLineDeletionBackground: #300406;
- --suggestedChangeDeletionText: #ffffff;
- --suggestedChangeAdditionText: #ffffff;
- --suggestedChangeDeletionBackground: rgba(218,54,51,0.6);
- --suggestedChangeAdditionBackground: rgba(46,160,67,0.6);
- --videoBackground: #000000;
-}
-
-/* Custom Base Styles */
-
-:root {
- --link-highlight: rgba(46, 143, 255, 0.08);
- --pre-background: var(--backgroundElevatedTertiary);
- --code-background: var(--pre-background);
- --hr-background: var(--borderOpaque);
- --thead-background: var(--pre-background);
- --thead-border: var(--hr-background);
- --tr-border: var(--gray-300);
- --tr-alt-background: var(--pre-background);
- --kbd-background: var(--pre-background);
- --kbd-color: var(--gray-350);
- --kbd-border: var(--gray-750);
- --blockquote-color: var(--gray-400);
- --blockquote-border: var(--hr-background);
- --heading-color: var(--gray-100);
- --h6-color: var(--gray-500);
- --frame-border: var(--hr-background);
- --frame-color: var(--gray-200);
- --mention-color: var(--textPrimary);
- --email-toggle-color: var(--kbd-color);
- --email-toggle-background: var(--blockquote-border);
- --email-quoted-color: var(--blockquote-color);
- --keyword-color: var(--gray-600);
- --code-font: ui-monospace, Menlo, monospace;
-}
diff --git a/app/src/main/assets/webview/colors_light.css b/app/src/main/assets/webview/colors_light.css
deleted file mode 100644
index 25eb8e445..000000000
--- a/app/src/main/assets/webview/colors_light.css
+++ /dev/null
@@ -1,156 +0,0 @@
-/* Primer Colors */
-/* Please also update colors_dark.css with dark mode appropriate colors when modifying this file. */
-
-:root {
- --blue-900: #05264c;
- --blue-800: #032f62;
- --blue-700: #044289;
- --blue-600: #005cc5;
- --blue-500: #0366d6;
- --blue-400: #2188ff;
- --blue-300: #79b8ff;
- --blue-200: #c8e1ff;
- --blue-100: #dbedff;
- --blue-000: #f1f8ff;
- --gray-1000: #050505;
- --gray-950: #0b0b0d;
- --gray-900: #17181a;
- --gray-850: #242528;
- --gray-800: #2f3037;
- --gray-750: #383a42;
- --gray-700: #41434e;
- --gray-650: #4b4d58;
- --gray-600: #525560;
- --gray-550: #5e616e;
- --gray-500: #6a6d7c;
- --gray-450: #787c8c;
- --gray-400: #9194a1;
- --gray-350: #a9abb6;
- --gray-300: #bfc1c9;
- --gray-250: #d6d7dc;
- --gray-200: #e3e4e8;
- --gray-150: #eff0f5;
- --gray-100: #f7f7f9;
- --gray-050: #fbfbfc;
- --gray-000: #ffffff;
- --green-900: #144620;
- --green-800: #165c26;
- --green-700: #176f2c;
- --green-600: #22863a;
- --green-500: #28a745;
- --green-400: #34d058;
- --green-300: #85e89d;
- --green-200: #bef5cb;
- --green-100: #dcffe4;
- --green-000: #f0fff4;
- --yellow-900: #735c0f;
- --yellow-800: #b08800;
- --yellow-700: #dbab09;
- --yellow-600: #f9c513;
- --yellow-500: #ffd33d;
- --yellow-400: #ffdf5d;
- --yellow-300: #ffea7f;
- --yellow-200: #fff5b1;
- --yellow-100: #fffbdd;
- --yellow-000: #fffdef;
- --orange-900: #a04100;
- --orange-800: #c24e00;
- --orange-700: #d15704;
- --orange-600: #e36209;
- --orange-500: #f66a0a;
- --orange-400: #fb8532;
- --orange-300: #ffab70;
- --orange-200: #ffd1ac;
- --orange-100: #ffebda;
- --orange-000: #fff8f2;
- --red-900: #86181d;
- --red-800: #9e1c23;
- --red-700: #b31d28;
- --red-600: #cb2431;
- --red-500: #d73a49;
- --red-400: #ea4a5a;
- --red-300: #f97583;
- --red-200: #fdaeb7;
- --red-100: #ffdce0;
- --red-000: #ffeef0;
- --pink-900: #6d224f;
- --pink-800: #99306f;
- --pink-700: #b93a86;
- --pink-600: #d03592;
- --pink-500: #ea4aaa;
- --pink-400: #ec6cb9;
- --pink-300: #f692ce;
- --pink-200: #f9b3dd;
- --pink-100: #fedbf0;
- --pink-000: #ffeef8;
- --purple-900: #29134e;
- --purple-800: #3a1d6e;
- --purple-700: #4c2888;
- --purple-600: #5a32a3;
- --purple-500: #6f42c1;
- --purple-400: #8a63d2;
- --purple-300: #b392f0;
- --purple-200: #d1bcf9;
- --purple-100: #e6dcfd;
- --purple-000: #f5f0ff;
- --textPrimary: var(--gray-1000);
- --textSecondary: var(--gray-700);
- --textTertiary: var(--gray-500);
- --textPlaceholder: rgba(82, 85, 96, 0.5);
- --link: var(--blue-500);
- --appBackground: var(--gray-000);
- --backgroundSecondary: var(--gray-000);
- --backgroundTertiary: var(--gray-000);
- --border: rgba(65, 67, 78, 0.25);
- --borderOpaque: var(--gray-300);
- --iconPrimary: var(--gray-600);
- --iconSecondary: var(--gray-400);
- --inputBackground: rgba(65, 67, 78, 0.12);
- --backgroundPrimary: var(--gray-150);
- --backgroundElevatedPrimary: var(--gray-150);
- --backgroundElevatedSecondary: var(--gray-000);
- --backgroundElevatedTertiary: var(--gray-000);
- --backgroundInset: var(--gray-200);
- --link-hover: var(--gray-200);
- --color-icon-success: var(--green-600);
- --color-text-danger: var(--red-600);
- --diffLineNumberAdditionBackground: #dcffe4;
- --diffLineNumberAdditionText: #22863a;
- --diffLineAdditionBackground: #f0fff4;
- --diffLineNumberDeletionBackground: #ffdce0;
- --diffLineNumberDeletionText: #cb2431;
- --diffLineDeletionBackground: #ffeef0;
- --suggestedChangeDeletionText: #ffffff;
- --suggestedChangeAdditionText: #ffffff;
- --suggestedChangeDeletionBackground: rgba(218,54,51,0.6);
- --suggestedChangeAdditionBackground: rgba(46,160,67,0.6);
- --videoBackground: #000000;
-}
-
-/* Custom Base Styles */
-
-:root {
- --link-highlight: rgba(3, 102, 214, 0.08);
- --pre-background: var(--gray-100);
- --code-background: var(--pre-background);
- --hr-background: var(--gray-200);
- --thead-background: var(--pre-background);
- --thead-border: var(--hr-background);
- --tr-border: var(--gray-300);
- --tr-alt-background: var(--pre-background);
- --kbd-background: var(--pre-background);
- --kbd-color: var(--gray-650);
- --kbd-border: var(--gray-250);
- --blockquote-color: var(--gray-500);
- --blockquote-border: var(--hr-background);
- --heading-color: var(--gray-900);
- --h6-color: var(--gray-500);
- --frame-border: var(--hr-background);
- --frame-color: var(--gray-850);
- --mention-color: var(--gray-850);
- --email-toggle-color: var(--kbd-color);
- --email-toggle-background: var(--blockquote-border);
- --email-quoted-color: var(--blockquote-color);
- --keyword-color: var(--gray-400);
- --code-font: ui-monospace, Menlo, monospace;
-}
diff --git a/app/src/main/assets/webview/markdown.css b/app/src/main/assets/webview/markdown.css
deleted file mode 100644
index 684aa4816..000000000
--- a/app/src/main/assets/webview/markdown.css
+++ /dev/null
@@ -1,589 +0,0 @@
-/* Shared styles between light & dark mode so all colors should be variables */
-
-* {
- box-sizing: border-box;
-}
-
-input:disabled {
- touch-action: none;
-}
-
-html {
- -webkit-text-size-adjust: none;
- text-size-adjust: none;
- font: -apple-system-body;
-}
-
-body {
- color: var(--textPrimary);
- background-color: var(--background);
-}
-
-a {
- color: var(--link);
- text-decoration: none;
- -webkit-tap-highlight-color: var(--link-highlight);
- word-break: break-word;
-}
-
-a:not([target]):hover {
- border-radius: 5px;
- background-color: var(--link-hover);
- transition-duration: 0.2s;
- transform: scale(1.015);
-}
-
-/*
-Web views hold on to their hover event if the app is backgrounded. We need to disable custom hover effects by setting a
-class on body and overriding them in CSS when we apply this workaround. When the mouse enters the web view again, we
-can disable our override.
-*/
-body.hover-override a:not([target]) {
- background-color: transparent;
- transform: scale(1);
-}
-
-details summary {
- outline: 0;
-}
-
-table {
- border-spacing: 0;
- border-collapse: collapse;
-}
-
-blockquote {
- margin: 0;
-}
-
-table, table *, pre {
- touch-action: pan-x;
-}
-
-.markdown-body ul.contains-task-list {
- list-style: none;
- padding-left: 0;
-}
-
-.task-list-item {
- padding-left: 40px;
- margin-left: -16px;
-}
-
-.task-list-item-checkbox {
- margin-left: -24px
-}
-
-pre, code, kbd {
- font-size: 1em;
- font-family: var(--code-font);
-}
-
-.issue-keyword {
- border-bottom: 1px dotted var(--keyword-color);
-}
-
-.team-mention, .user-mention {
- font-weight: 600;
- color: var(--mention-color);
- white-space: nowrap;
-}
-
-.email-hidden-toggle, .email-hidden-reply {
- display: none;
-}
-
-/* Fix checkboxes looking cut off when they render larger than the default size */
-input[type="checkbox"] {
- transform: translate(0px);
-}
-
-/* --- */
-
-.markdown-body {
- font-size: inherit;
- line-height: 1.5;
- word-wrap: break-word;
-}
-
-.markdown-body kbd {
- display: inline-block;
- padding: 0.18em 0.31em;
- font-size: 0.7em;
- line-height: 1.2em;
- color: var(--kbd-color);
- vertical-align: middle;
- background-color: var(--kbd-background);
- border: 1px solid var(--kbd-border);
- border-radius: 0.25em;
- box-shadow: inset 0 -1px 0 var(--kbd-border);
- margin-right: 2px;
-}
-
-.markdown-body:after, .markdown-body:before {
- display: table;
- content: ""
-}
-
-.markdown-body:after {
- clear: both;
-}
-
-.markdown-body > :first-child {
- margin-top: 0 !important;
-}
-
-.markdown-body > :last-child {
- margin-bottom: 0 !important;
-}
-
-.markdown-body a:not([href]) {
- color: inherit;
- text-decoration: none;
-}
-
-.markdown-body .absent {
- color: var(--red-600);
-}
-
-/* GitHub now emits the heading permalink anchor as a sibling AFTER the
- heading (not a child), so the old ".../h6 .octicon-link" hide rule no
- longer matches it and the floated icon leaks into the left gutter of the
- following block. These permalinks are useless in this viewer (no hover,
- no address bar), so hide them outright. */
-.markdown-body .anchor {
- display: none;
-}
-
-.markdown-body .anchor:focus {
- outline: none;
-}
-
-.markdown-body blockquote, .markdown-body details, .markdown-body dl, .markdown-body ol, .markdown-body p, .markdown-body pre, .markdown-body table, .markdown-body ul {
- margin-top: 0;
- margin-bottom: 16px;
-}
-
-.markdown-body hr {
- height: .25em;
- padding: 0;
- margin: 24px 0;
- background-color: var(--hr-background);
- border: 0;
-}
-
-.markdown-body blockquote {
- padding-left: 1em;
- color: var(--blockquote-color);
- position: relative;
-}
-
-.markdown-body blockquote::before {
- content: '';
- width: 2px;
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- background-color: var(--blockquote-border);
- border-radius: 2px;
-}
-
-.markdown-body blockquote > :first-child {
- margin-top: 0;
-}
-
-.markdown-body blockquote > :last-child {
- margin-bottom: 0;
-}
-
-.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
- margin-top: 24px;
- margin-bottom: 16px;
- font-weight: 600;
- line-height: 1.25;
-}
-
-.markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link {
- color: var(--heading-color);
- vertical-align: middle;
- visibility: hidden;
-}
-
-.markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor {
- text-decoration: none;
-}
-
-.markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link {
- visibility: visible;
-}
-
-.markdown-body h1 code, .markdown-body h1 tt, .markdown-body h2 code, .markdown-body h2 tt, .markdown-body h3 code, .markdown-body h3 tt, .markdown-body h4 code, .markdown-body h4 tt, .markdown-body h5 code, .markdown-body h5 tt, .markdown-body h6 code, .markdown-body h6 tt {
- font-size: inherit;
-}
-
-.markdown-body h1 {
- font-size: 2em;
-}
-
-.markdown-body h1, .markdown-body h2 {
- padding-bottom: .3em;
- border-bottom: 1px solid var(--border);
-}
-
-.markdown-body h2 {
- font-size: 1.5em;
-}
-
-.markdown-body h3 {
- font-size: 1.25em;
-}
-
-.markdown-body h4 {
- font-size: 1em;
-}
-
-.markdown-body h5 {
- font-size: .875em;
-}
-
-.markdown-body h6 {
- font-size: .85em;
- color: var(--h6-color);
-}
-
-.markdown-body ul {
- padding-left: 1.5em;
-}
-
-.markdown-body ol.no-list, .markdown-body ul.no-list {
- padding: 0;
- list-style-type: none;
-}
-
-.markdown-body ol ol, .markdown-body ol ul, .markdown-body ul ol, .markdown-body ul ul {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-.markdown-body li {
- word-wrap: break-all;
-}
-
-.markdown-body li > p {
- margin-top: 16px;
-}
-
-.markdown-body li + li {
- margin-top: .25em;
-}
-
-.markdown-body dl {
- padding: 0;
-}
-
-.markdown-body dl dt {
- padding: 0;
- margin-top: 16px;
- font-size: 1em;
- font-style: italic;
- font-weight: 600;
-}
-
-.markdown-body dl dd {
- padding: 0 16px;
- margin-bottom: 16px;
-}
-
-.markdown-body table {
- display: block;
- width: 100%;
- overflow: auto;
-}
-
-.markdown-body table th {
- font-weight: 600;
-}
-
-.markdown-body table td, .markdown-body table th {
- padding: 6px 13px;
- border: 1px solid var(--thead-border);
-}
-
-.markdown-body table tr {
- background-color: var(--background);
- border-top: 1px solid var(--tr-border);
-}
-
-.markdown-body table tr:nth-child(2n) {
- background-color: var(--tr-alt-background);
-}
-
-.markdown-body table img {
- background-color: initial;
-}
-
-.markdown-body img {
- max-width: 100%;
- box-sizing: initial;
- background-color: var(--background);
-}
-
-.markdown-body img[align=right] {
- padding-left: 20px;
-}
-
-.markdown-body img[align=left] {
- padding-right: 20px;
-}
-
-.markdown-body video {
- max-width: 100%;
- box-sizing: initial;
- background-color: var(--videoBackground);
-}
-
-.markdown-body .emoji {
- max-width: none;
- vertical-align: text-top;
- background-color: initial;
-}
-
-.markdown-body span.frame {
- display: block;
- overflow: hidden;
-}
-
-.markdown-body span.frame > span {
- display: block;
- float: left;
- width: auto;
- padding: 7px;
- margin: 13px 0 0;
- overflow: hidden;
- border: 1px solid var(--frame-border);
-}
-
-.markdown-body span.frame span img {
- display: block;
- float: left;
-}
-
-.markdown-body span.frame span span {
- display: block;
- padding: 5px 0 0;
- clear: both;
- color: var(--frame-color);
-}
-
-.markdown-body span.align-center {
- display: block;
- overflow: hidden;
- clear: both;
-}
-
-.markdown-body span.align-center > span {
- display: block;
- margin: 13px auto 0;
- overflow: hidden;
- text-align: center;
-}
-
-.markdown-body span.align-center span img {
- margin: 0 auto;
- text-align: center;
-}
-
-.markdown-body span.align-right {
- display: block;
- overflow: hidden;
- clear: both;
-}
-
-.markdown-body span.align-right > span {
- display: block;
- margin: 13px 0 0;
- overflow: hidden;
- text-align: right;
-}
-
-.markdown-body span.align-right span img {
- margin: 0;
- text-align: right;
-}
-
-.markdown-body span.float-left {
- display: block;
- float: left;
- margin-right: 13px;
- overflow: hidden;
-}
-
-.markdown-body span.float-left span {
- margin: 13px 0 0;
-}
-
-.markdown-body span.float-right {
- display: block;
- float: right;
- margin-left: 13px;
- overflow: hidden;
-}
-
-.markdown-body span.float-right > span {
- display: block;
- margin: 13px auto 0;
- overflow: hidden;
- text-align: right;
-}
-
-.markdown-body code, .markdown-body tt {
- padding: .2em .4em;
- margin: 0;
- font-size: 85%;
- background-color: var(--code-background);
- border-radius: 6px;
-}
-
-.markdown-body code br, .markdown-body tt br {
- display: none;
-}
-
-.markdown-body del code {
- text-decoration: inherit;
-}
-
-.markdown-body pre {
- word-wrap: normal;
-}
-
-.markdown-body pre > code {
- padding: 0;
- margin: 0;
- font-size: 100%;
- word-break: normal;
- white-space: pre;
- background: transparent;
- border: 0;
-}
-
-.markdown-body .highlight {
- margin-bottom: 16px;
-}
-
-.markdown-body .highlight pre {
- margin-bottom: 0;
- word-break: normal;
-}
-
-.markdown-body .highlight pre, .markdown-body pre {
- padding: 16px;
- overflow: auto;
- font-size: 85%;
- line-height: 1.45;
- background-color: var(--pre-background);
- border-radius: 6px;
-}
-
-.markdown-body pre code, .markdown-body pre tt {
- display: inline;
- max-width: auto;
- padding: 0;
- margin: 0;
- overflow: visible;
- line-height: inherit;
- word-wrap: normal;
- background-color: initial;
- border: 0;
-}
-
-.markdown-body .csv-data td, .markdown-body .csv-data th {
- padding: 5px;
- overflow: hidden;
- font-size: 12px;
- line-height: 1;
- text-align: left;
- white-space: nowrap;
-}
-
-.markdown-body .csv-data .blob-num {
- padding: 10px 8px 9px;
- text-align: right;
- background: var(--background);
- border: 0;
-}
-
-.markdown-body .csv-data tr {
- border-top: 0;
-}
-
-.markdown-body .csv-data th {
- font-weight: 600;
- background: var(--thead-background);
- border-top: 0;
-}
-
-.open.octicon, .draft.octicon, .closed.octicon, .merged.octicon, .color-text-secondary.octicon {
- display: inline-block;
- margin-top: 0.15em;
- vertical-align: text-top;
- fill: currentColor;
- width: 1em;
- height: 1em;
- font: -apple-system-body;
-}
-
-.open.octicon {
- color: var(--color-icon-success);
-}
-
-.draft.octicon {
- color: var(--textTertiary);
-}
-
-.closed.octicon {
- color: var(--color-text-danger);
-}
-
-.merged.octicon {
- color: var(--purple-500);
-}
-
-.color-text-secondary.octicon {
- color: var(--textSecondary);
-}
-
-.reference {
- white-space: nowrap;
-}
-
-.issue-link {
- font-weight: 600;
- color: var(--mention-color);
- white-space: normal;
-}
-
-.issue-shorthand {
- font-weight: 400;
- color: var(--textTertiary);
-}
-
-.mr-1 {
- margin-right: 4px;
-}
-
-.ml-1 {
- margin-left: 4px;
-}
-
-.d-inline-block {
- display: inline-block;
-}
-
-.v-align-middle {
- vertical-align: middle;
-}
-
-.Box {
- border-radius: 6px;
-}
diff --git a/app/src/main/assets/webview/syntax.css b/app/src/main/assets/webview/syntax.css
deleted file mode 100644
index 727766652..000000000
--- a/app/src/main/assets/webview/syntax.css
+++ /dev/null
@@ -1,124 +0,0 @@
-/* From https://github.com/primer/github-syntax-light/blob/master/lib/github-light.css */
-.pl-c /* comment, punctuation.definition.comment, string.comment */ {
- color: #6a737d;
-}
-
-.pl-c1 /* constant, entity.name.constant, variable.other.constant, variable.language, support, meta.property-name, support.constant, support.variable, meta.module-reference, markup.raw, meta.diff.header, meta.output */,
-.pl-s .pl-v /* string variable */ {
- color: #005cc5;
-}
-
-.pl-e /* entity */,
-.pl-en /* entity.name */ {
- color: #6f42c1;
-}
-
-.pl-smi /* variable.parameter.function, storage.modifier.package, storage.modifier.import, storage.type.java, variable.other */,
-.pl-s .pl-s1 /* string source */ {
- color: #24292e;
-}
-
-.pl-ent /* entity.name.tag, markup.quote */ {
- color: #22863a;
-}
-
-.pl-k /* keyword, storage, storage.type */ {
- color: #d73a49;
-}
-
-.pl-s /* string */,
-.pl-pds /* punctuation.definition.string, source.regexp, string.regexp.character-class */,
-.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
-.pl-sr /* string.regexp */,
-.pl-sr .pl-cce /* string.regexp constant.character.escape */,
-.pl-sr .pl-sre /* string.regexp source.ruby.embedded */,
-.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */ {
- color: #032f62;
-}
-
-.pl-v /* variable */,
-.pl-smw /* sublimelinter.mark.warning */ {
- color: #e36209;
-}
-
-.pl-bu /* invalid.broken, invalid.deprecated, invalid.unimplemented, message.error, brackethighlighter.unmatched, sublimelinter.mark.error */ {
- color: #b31d28;
-}
-
-.pl-ii /* invalid.illegal */ {
- color: #fafbfc;
- background-color: #b31d28;
-}
-
-.pl-c2 /* carriage-return */ {
- color: #fafbfc;
- background-color: #d73a49;
-}
-
-.pl-c2::before /* carriage-return */ {
- content: "^M";
-}
-
-.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
- font-weight: bold;
- color: #22863a;
-}
-
-.pl-ml /* markup.list */ {
- color: #735c0f;
-}
-
-.pl-mh /* markup.heading */,
-.pl-mh .pl-en /* markup.heading entity.name */,
-.pl-ms /* meta.separator */ {
- font-weight: bold;
- color: #005cc5;
-}
-
-.pl-mi /* markup.italic */ {
- font-style: italic;
- color: #24292e;
-}
-
-.pl-mb /* markup.bold */ {
- font-weight: bold;
- color: #24292e;
-}
-
-.pl-md /* markup.deleted, meta.diff.header.from-file, punctuation.definition.deleted */ {
- color: #b31d28;
- background-color: #ffeef0;
-}
-
-.pl-mi1 /* markup.inserted, meta.diff.header.to-file, punctuation.definition.inserted */ {
- color: #22863a;
- background-color: #f0fff4;
-}
-
-.pl-mc /* markup.changed, punctuation.definition.changed */ {
- color: #e36209;
- background-color: #ffebda;
-}
-
-.pl-mi2 /* markup.ignored, markup.untracked */ {
- color: #f6f8fa;
- background-color: #005cc5;
-}
-
-.pl-mdr /* meta.diff.range */ {
- font-weight: bold;
- color: #6f42c1;
-}
-
-.pl-ba /* brackethighlighter.tag, brackethighlighter.curly, brackethighlighter.round, brackethighlighter.square, brackethighlighter.angle, brackethighlighter.quote */ {
- color: #586069;
-}
-
-.pl-sg /* sublimelinter.gutter-mark */ {
- color: #959da5;
-}
-
-.pl-corl /* constant.other.reference.link, string.other.link */ {
- text-decoration: underline;
- color: #032f62;
-}
diff --git a/app/src/main/assets/webview/syntax_dark.css b/app/src/main/assets/webview/syntax_dark.css
deleted file mode 100644
index e7858b39c..000000000
--- a/app/src/main/assets/webview/syntax_dark.css
+++ /dev/null
@@ -1,124 +0,0 @@
-/* From https://github.com/primer/github-syntax-dark/blob/master/lib/github-dark.css */
-.pl-c /* comment, punctuation.definition.comment, string.comment */ {
- color: #959da5;
-}
-
-.pl-c1 /* constant, entity.name.constant, variable.other.constant, variable.language, support, meta.property-name, support.constant, support.variable, meta.module-reference, markup.quote, markup.raw, meta.diff.header */,
-.pl-s .pl-v /* string variable */ {
- color: #c8e1ff;
-}
-
-.pl-e /* entity */,
-.pl-en /* entity.name */ {
- color: #b392f0;
-}
-
-.pl-smi /* variable.parameter.function, storage.modifier.package, storage.modifier.import, storage.type.java, variable.other */,
-.pl-s .pl-s1 /* string source */ {
- color: #f6f8fa;
-}
-
-.pl-ent /* entity.name.tag */ {
- color: #7bcc72;
-}
-
-.pl-k /* keyword, storage, storage.type */ {
- color: #ea4a5a;
-}
-
-.pl-s /* string */,
-.pl-pds /* punctuation.definition.string, source.regexp, string.regexp.character-class */,
-.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
-.pl-sr /* string.regexp */,
-.pl-sr .pl-cce /* string.regexp constant.character.escape */,
-.pl-sr .pl-sre /* string.regexp source.ruby.embedded */,
-.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */ {
- color: #79b8ff;
-}
-
-.pl-v /* variable */,
-.pl-ml /* markup.list, sublimelinter.mark.warning */ {
- color: #fb8532;
-}
-
-.pl-bu /* invalid.broken, invalid.deprecated, invalid.unimplemented, message.error, brackethighlighter.unmatched, sublimelinter.mark.error */ {
- color: #d73a49;
-}
-
-.pl-ii /* invalid.illegal */ {
- color: #fafbfc;
- background-color: #d73a49;
-}
-
-.pl-c2 /* carriage-return */ {
- color: #fafbfc;
- background-color: #d73a49;
-}
-
-.pl-c2::before /* carriage-return */ {
- content: "^M";
-}
-
-.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
- font-weight: bold;
- color: #7bcc72;
-}
-
-.pl-mh /* markup.heading */,
-.pl-mh .pl-en /* markup.heading entity.name */,
-.pl-ms /* meta.separator */ {
- font-weight: bold;
- color: #0366d6;
-}
-
-.pl-mi /* markup.italic */ {
- font-style: italic;
- color: #f6f8fa;
-}
-
-.pl-mb /* markup.bold */ {
- font-weight: bold;
- color: #f6f8fa;
-}
-
-.pl-md /* markup.deleted, meta.diff.header.from-file, punctuation.definition.deleted */ {
- color: #ffdcd7;
- background-color: #67060c;
-}
-
-.pl-mi1 /* markup.inserted, meta.diff.header.to-file, punctuation.definition.inserted */ {
- color: #aff5b4;
- background-color: #033a16;
-}
-
-.pl-mc /* markup.changed, punctuation.definition.changed */ {
- color: #b08800;
- background-color: #fffdef;
-}
-
-.pl-mi2 /* markup.ignored, markup.untracked */ {
- color: #2f363d;
- background-color: #959da5;
-}
-
-.pl-mdr /* meta.diff.range */ {
- font-weight: bold;
- color: #b392f0;
-}
-
-.pl-mo /* meta.output */ {
- color: #0366d6;
-}
-
-.pl-ba /* brackethighlighter.tag, brackethighlighter.curly, brackethighlighter.round, brackethighlighter.square, brackethighlighter.angle, brackethighlighter.quote */ {
- color: #ffeef0;
-}
-
-.pl-sg /* sublimelinter.gutter-mark */ {
- color: #6a737d;
-}
-
-.pl-corl /* constant.other.reference.link, string.other.link */ {
- text-decoration: underline;
- color: #79b8ff;
-}
diff --git a/app/src/main/assets/webview/template.html b/app/src/main/assets/webview/template.html
deleted file mode 100644
index c6c9e00f5..000000000
--- a/app/src/main/assets/webview/template.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- @body@
-
-
diff --git a/app/src/main/assets/webview/template_dark.html b/app/src/main/assets/webview/template_dark.html
deleted file mode 100644
index 964ba6abb..000000000
--- a/app/src/main/assets/webview/template_dark.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- @body@
-
-
diff --git a/app/src/main/java/com/google/android/material/appbar/SubtitleCollapsingToolbarLayout.java b/app/src/main/java/com/google/android/material/appbar/SubtitleCollapsingToolbarLayout.java
deleted file mode 100644
index f2207f1ac..000000000
--- a/app/src/main/java/com/google/android/material/appbar/SubtitleCollapsingToolbarLayout.java
+++ /dev/null
@@ -1,1263 +0,0 @@
-package com.google.android.material.appbar;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.widget.FrameLayout;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.StyleRes;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.core.math.MathUtils;
-import androidx.core.view.GravityCompat;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowInsetsCompat;
-
-import com.google.android.material.animation.AnimationUtils;
-import com.google.android.material.internal.DescendantOffsetUtils;
-import com.google.android.material.internal.SubtitleCollapsingTextHelper;
-import com.google.android.material.internal.ThemeEnforcement;
-
-import org.lsposed.manager.R;
-
-/**
- * @see CollapsingToolbarLayout
- */
-public class SubtitleCollapsingToolbarLayout extends FrameLayout {
-
- private static final int DEFAULT_SCRIM_ANIMATION_DURATION = 600;
-
- private boolean refreshToolbar = true;
- private int toolbarId;
- @Nullable
- private Toolbar toolbar;
- @Nullable
- private View toolbarDirectChild;
- private View dummyView;
-
- private int expandedMarginStart;
- private int expandedMarginTop;
- private int expandedMarginEnd;
- private int expandedMarginBottom;
-
- private final Rect tmpRect = new Rect();
- @NonNull
- final SubtitleCollapsingTextHelper collapsingTextHelper;
- private boolean collapsingTitleEnabled;
- private boolean drawCollapsingTitle;
-
- @Nullable
- private Drawable contentScrim;
- @Nullable
- Drawable statusBarScrim;
- private int scrimAlpha;
- private boolean scrimsAreShown;
- private ValueAnimator scrimAnimator;
- private long scrimAnimationDuration;
- private int scrimVisibleHeightTrigger = -1;
-
- private AppBarLayout.OnOffsetChangedListener onOffsetChangedListener;
-
- int currentOffset;
-
- @Nullable
- WindowInsetsCompat lastInsets;
-
- public SubtitleCollapsingToolbarLayout(@NonNull Context context) {
- this(context, null);
- }
-
- public SubtitleCollapsingToolbarLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public SubtitleCollapsingToolbarLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- collapsingTextHelper = new SubtitleCollapsingTextHelper(this);
- collapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
- collapsingTextHelper.setRtlTextDirectionHeuristicsEnabled(false);
-
- TypedArray a = ThemeEnforcement.obtainStyledAttributes(
- context,
- attrs,
- R.styleable.SubtitleCollapsingToolbarLayout,
- defStyleAttr,
- R.style.Widget_Design_SubtitleCollapsingToolbar);
-
- collapsingTextHelper.setExpandedTextGravity(a.getInt(
- R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleGravity,
- GravityCompat.START | Gravity.BOTTOM));
- collapsingTextHelper.setCollapsedTextGravity(a.getInt(
- R.styleable.SubtitleCollapsingToolbarLayout_collapsedTitleGravity,
- GravityCompat.START | Gravity.CENTER_VERTICAL));
-
- expandedMarginStart = expandedMarginTop = expandedMarginEnd = expandedMarginBottom =
- a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMargin, 0);
-
- if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginStart)) {
- expandedMarginStart =
- a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginStart, 0);
- }
- if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd)) {
- expandedMarginEnd =
- a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd, 0);
- }
- if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginTop)) {
- expandedMarginTop =
- a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginTop, 0);
- }
- if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginBottom)) {
- expandedMarginBottom =
- a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginBottom, 0);
- }
-
- collapsingTitleEnabled = a.getBoolean(R.styleable.SubtitleCollapsingToolbarLayout_titleEnabled, true);
- setTitle(a.getText(R.styleable.SubtitleCollapsingToolbarLayout_title));
- setSubtitle(a.getText(R.styleable.SubtitleCollapsingToolbarLayout_subtitle));
-
- // First load the default text appearances
- collapsingTextHelper.setExpandedTitleTextAppearance(
- R.style.TextAppearance_Design_SubtitleCollapsingToolbar_ExpandedTitle);
- collapsingTextHelper.setCollapsedTitleTextAppearance(
- androidx.appcompat.R.style.TextAppearance_AppCompat_Widget_ActionBar_Title);
- collapsingTextHelper.setExpandedSubtitleTextAppearance(
- R.style.TextAppearance_Design_SubtitleCollapsingToolbar_ExpandedSubtitle);
- collapsingTextHelper.setCollapsedSubtitleTextAppearance(
- androidx.appcompat.R.style.TextAppearance_AppCompat_Widget_ActionBar_Subtitle);
-
- // Now overlay any custom text appearances
- if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleTextAppearance)) {
- collapsingTextHelper.setExpandedTitleTextAppearance(
- a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleTextAppearance, 0));
- }
- if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_collapsedTitleTextAppearance)) {
- collapsingTextHelper.setCollapsedTitleTextAppearance(
- a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_collapsedTitleTextAppearance, 0));
- }
- if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedSubtitleTextAppearance)) {
- collapsingTextHelper.setExpandedSubtitleTextAppearance(
- a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_expandedSubtitleTextAppearance, 0));
- }
- if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_collapsedSubtitleTextAppearance)) {
- collapsingTextHelper.setCollapsedSubtitleTextAppearance(
- a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_collapsedSubtitleTextAppearance, 0));
- }
-
- scrimVisibleHeightTrigger = a
- .getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_scrimVisibleHeightTrigger, -1);
-
- scrimAnimationDuration = a.getInt(
- R.styleable.SubtitleCollapsingToolbarLayout_scrimAnimationDuration,
- DEFAULT_SCRIM_ANIMATION_DURATION);
-
- setContentScrim(a.getDrawable(R.styleable.SubtitleCollapsingToolbarLayout_contentScrim));
- setStatusBarScrim(a.getDrawable(R.styleable.SubtitleCollapsingToolbarLayout_statusBarScrim));
-
- toolbarId = a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_toolbarId, -1);
-
- a.recycle();
-
- setWillNotDraw(false);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- // Add an OnOffsetChangedListener if possible
- final ViewParent parent = getParent();
- if (parent instanceof AppBarLayout) {
- // Copy over from the ABL whether we should fit system windows
- ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent));
-
- if (onOffsetChangedListener == null) {
- onOffsetChangedListener = new OffsetUpdateListener();
- }
- ((AppBarLayout) parent).addOnOffsetChangedListener(onOffsetChangedListener);
-
- // We're attached, so lets request an inset dispatch
- ViewCompat.requestApplyInsets(this);
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- // Remove our OnOffsetChangedListener if possible and it exists
- final ViewParent parent = getParent();
- if (onOffsetChangedListener != null && parent instanceof AppBarLayout) {
- ((AppBarLayout) parent).removeOnOffsetChangedListener(onOffsetChangedListener);
- }
-
- super.onDetachedFromWindow();
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- super.draw(canvas);
-
- // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below.
- // Instead, we draw it here, before our collapsing text.
- ensureToolbar();
- if (toolbar == null && contentScrim != null && scrimAlpha > 0) {
- contentScrim.mutate().setAlpha(scrimAlpha);
- contentScrim.draw(canvas);
- }
-
- // Let the collapsing text helper draw its text
- if (collapsingTitleEnabled && drawCollapsingTitle) {
- collapsingTextHelper.draw(canvas);
- }
-
- // Now draw the status bar scrim
- if (statusBarScrim != null && scrimAlpha > 0) {
- final int topInset = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0;
- if (topInset > 0) {
- statusBarScrim.setBounds(0, -currentOffset, getWidth(), topInset - currentOffset);
- statusBarScrim.mutate().setAlpha(scrimAlpha);
- statusBarScrim.draw(canvas);
- }
- }
- }
-
- @Override
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present),
- // but in front of any other children which are behind it. To do this we intercept the
- // drawChild() call, and draw our scrim just before the Toolbar is drawn
- boolean invalidated = false;
- if (contentScrim != null && scrimAlpha > 0 && isToolbarChild(child)) {
- contentScrim.mutate().setAlpha(scrimAlpha);
- contentScrim.draw(canvas);
- invalidated = true;
- }
- return super.drawChild(canvas, child, drawingTime) || invalidated;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- if (contentScrim != null) {
- contentScrim.setBounds(0, 0, w, h);
- }
- }
-
- private void ensureToolbar() {
- if (!refreshToolbar) {
- return;
- }
-
- // First clear out the current Toolbar
- this.toolbar = null;
- toolbarDirectChild = null;
-
- if (toolbarId != -1) {
- // If we have an ID set, try and find it and it's direct parent to us
- this.toolbar = findViewById(toolbarId);
- if (this.toolbar != null) {
- toolbarDirectChild = findDirectChild(this.toolbar);
- }
- }
-
- if (this.toolbar == null) {
- // If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find
- // one from our direct children
- Toolbar toolbar = null;
- for (int i = 0, count = getChildCount(); i < count; i++) {
- final View child = getChildAt(i);
- if (child instanceof Toolbar) {
- toolbar = (Toolbar) child;
- break;
- }
- }
- this.toolbar = toolbar;
- }
-
- updateDummyView();
- refreshToolbar = false;
- }
-
- private boolean isToolbarChild(View child) {
- return (toolbarDirectChild == null || toolbarDirectChild == this)
- ? child == toolbar
- : child == toolbarDirectChild;
- }
-
- /**
- * Returns the direct child of this layout, which itself is the ancestor of the given view.
- */
- @NonNull
- private View findDirectChild(@NonNull final View descendant) {
- View directChild = descendant;
- for (ViewParent p = descendant.getParent(); p != this && p != null; p = p.getParent()) {
- if (p instanceof View) {
- directChild = (View) p;
- }
- }
- return directChild;
- }
-
- private void updateDummyView() {
- if (!collapsingTitleEnabled && dummyView != null) {
- // If we have a dummy view and we have our title disabled, remove it from its parent
- final ViewParent parent = dummyView.getParent();
- if (parent instanceof ViewGroup) {
- ((ViewGroup) parent).removeView(dummyView);
- }
- }
- if (collapsingTitleEnabled && toolbar != null) {
- if (dummyView == null) {
- dummyView = new View(getContext());
- }
- if (dummyView.getParent() == null) {
- toolbar.addView(dummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- }
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- ensureToolbar();
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- final int mode = MeasureSpec.getMode(heightMeasureSpec);
- final int topInset = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0;
- if (mode == MeasureSpec.UNSPECIFIED && topInset > 0) {
- // If we have a top inset and we're set to wrap_content height we need to make sure
- // we add the top inset to our height, therefore we re-measure
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() + topInset, MeasureSpec.EXACTLY);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- // Set our minimum height to enable proper AppBarLayout collapsing
- if (toolbar != null) {
- if (toolbarDirectChild == null || toolbarDirectChild == this) {
- setMinimumHeight(getHeightWithMargins(toolbar));
- } else {
- setMinimumHeight(getHeightWithMargins(toolbarDirectChild));
- }
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (lastInsets != null) {
- // Shift down any views which are not set to fit system windows
- final int insetTop = lastInsets.getSystemWindowInsetTop();
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- if (!ViewCompat.getFitsSystemWindows(child)) {
- if (child.getTop() < insetTop) {
- // If the child isn't set to fit system windows but is drawing within
- // the inset offset it down
- ViewCompat.offsetTopAndBottom(child, insetTop);
- }
- }
- }
- }
-
- // Update our child view offset helpers so that they track the correct layout coordinates
- for (int i = 0, z = getChildCount(); i < z; i++) {
- getViewOffsetHelper(getChildAt(i)).onViewLayout();
- }
-
- // Update the collapsed bounds by getting its transformed bounds
- if (collapsingTitleEnabled && dummyView != null) {
- // We only draw the title if the dummy view is being displayed (Toolbar removes
- // views if there is no space)
- drawCollapsingTitle = ViewCompat.isAttachedToWindow(dummyView) && dummyView.getVisibility() == VISIBLE;
-
- if (drawCollapsingTitle) {
- final boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
-
- // Update the collapsed bounds
- final int maxOffset =
- getMaxOffsetForPinChild(toolbarDirectChild != null ? toolbarDirectChild : toolbar);
- DescendantOffsetUtils.getDescendantRect(this, dummyView, tmpRect);
- collapsingTextHelper.setCollapsedBounds(
- tmpRect.left + (isRtl ? toolbar.getTitleMarginEnd() : toolbar.getTitleMarginStart()),
- tmpRect.top + maxOffset + toolbar.getTitleMarginTop(),
- tmpRect.right - (isRtl ? toolbar.getTitleMarginStart() : toolbar.getTitleMarginEnd()),
- tmpRect.bottom + maxOffset - toolbar.getTitleMarginBottom());
-
- // Update the expanded bounds
- collapsingTextHelper.setExpandedBounds(
- isRtl ? expandedMarginEnd : expandedMarginStart,
- tmpRect.top + expandedMarginTop,
- right - left - (isRtl ? expandedMarginStart : expandedMarginEnd),
- bottom - top - expandedMarginBottom);
- // Now recalculate using the new bounds
- collapsingTextHelper.recalculate();
- }
- }
-
- if (toolbar != null) {
- if (collapsingTitleEnabled && TextUtils.isEmpty(collapsingTextHelper.getTitle())) {
- // If we do not currently have a title, try and grab it from the Toolbar
- setTitle(toolbar.getTitle());
- setSubtitle(toolbar.getSubtitle());
- }
- }
-
- updateScrimVisibility();
-
- // Apply any view offsets, this should be done at the very end of layout
- for (int i = 0, z = getChildCount(); i < z; i++) {
- getViewOffsetHelper(getChildAt(i)).applyOffsets();
- }
- }
-
- private static int getHeightWithMargins(@NonNull final View view) {
- final ViewGroup.LayoutParams lp = view.getLayoutParams();
- if (lp instanceof MarginLayoutParams) {
- final MarginLayoutParams mlp = (MarginLayoutParams) lp;
- return view.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
- }
- return view.getMeasuredHeight();
- }
-
- static ViewOffsetHelper getViewOffsetHelper(View view) {
- ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(com.google.android.material.R.id.view_offset_helper);
- if (offsetHelper == null) {
- offsetHelper = new ViewOffsetHelper(view);
- view.setTag(com.google.android.material.R.id.view_offset_helper, offsetHelper);
- }
- return offsetHelper;
- }
-
- /**
- * Sets the title to be displayed by this view, if enabled.
- *
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_title
- * @see #setTitleEnabled(boolean)
- * @see #getTitle()
- */
- public void setTitle(@Nullable CharSequence title) {
- collapsingTextHelper.setTitle(title);
- updateContentDescriptionFromTitle();
- }
-
- /**
- * Returns the title currently being displayed by this view. If the title is not enabled, then
- * this will return {@code null}.
- *
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_title
- */
- @Nullable
- public CharSequence getTitle() {
- return collapsingTitleEnabled ? collapsingTextHelper.getTitle() : null;
- }
-
- /**
- * Sets the subtitle to be displayed by this view, if enabled.
- *
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_subtitle
- * @see #setTitleEnabled(boolean)
- * @see #getSubtitle()
- */
- public void setSubtitle(@Nullable CharSequence subtitle) {
- collapsingTextHelper.setSubtitle(subtitle);
- updateContentDescriptionFromTitle();
- }
-
- /**
- * Returns the subtitle currently being displayed by this view. If the title is not enabled, then
- * this will return {@code null}.
- *
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_subtitle
- */
- @Nullable
- public CharSequence getSubtitle() {
- return collapsingTitleEnabled ? collapsingTextHelper.getSubtitle() : null;
- }
-
- /**
- * Sets whether this view should display its own title and subtitle.
- *
- *
The title and subtitle displayed by this view will shrink and grow based on the scroll offset.
- *
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_titleEnabled
- * @see #setTitle(CharSequence)
- * @see #setSubtitle(CharSequence)
- * @see #isTitleEnabled()
- */
- public void setTitleEnabled(boolean enabled) {
- if (enabled != collapsingTitleEnabled) {
- collapsingTitleEnabled = enabled;
- updateContentDescriptionFromTitle();
- updateDummyView();
- requestLayout();
- }
- }
-
- /**
- * Returns whether this view is currently displaying its own title and subtitle.
- *
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_titleEnabled
- * @see #setTitleEnabled(boolean)
- */
- public boolean isTitleEnabled() {
- return collapsingTitleEnabled;
- }
-
- /**
- * Set whether the content scrim and/or status bar scrim should be shown or not. Any change in the
- * vertical scroll may overwrite this value. Any visibility change will be animated if this view
- * has already been laid out.
- *
- * @param shown whether the scrims should be shown
- * @see #getStatusBarScrim()
- * @see #getContentScrim()
- */
- public void setScrimsShown(boolean shown) {
- setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode());
- }
-
- /**
- * Set whether the content scrim and/or status bar scrim should be shown or not. Any change in the
- * vertical scroll may overwrite this value.
- *
- * @param shown whether the scrims should be shown
- * @param animate whether to animate the visibility change
- * @see #getStatusBarScrim()
- * @see #getContentScrim()
- */
- public void setScrimsShown(boolean shown, boolean animate) {
- if (scrimsAreShown != shown) {
- if (animate) {
- animateScrim(shown ? 0xFF : 0x0);
- } else {
- setScrimAlpha(shown ? 0xFF : 0x0);
- }
- scrimsAreShown = shown;
- }
- }
-
- private void animateScrim(int targetAlpha) {
- ensureToolbar();
- if (scrimAnimator == null) {
- scrimAnimator = new ValueAnimator();
- scrimAnimator.setDuration(scrimAnimationDuration);
- scrimAnimator.setInterpolator(targetAlpha > scrimAlpha
- ? AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR
- : AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
- scrimAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- setScrimAlpha((int) animator.getAnimatedValue());
- }
- });
- } else if (scrimAnimator.isRunning()) {
- scrimAnimator.cancel();
- }
-
- scrimAnimator.setIntValues(scrimAlpha, targetAlpha);
- scrimAnimator.start();
- }
-
- void setScrimAlpha(int alpha) {
- if (alpha != scrimAlpha) {
- final Drawable contentScrim = this.contentScrim;
- if (contentScrim != null && toolbar != null) {
- ViewCompat.postInvalidateOnAnimation(toolbar);
- }
- scrimAlpha = alpha;
- ViewCompat.postInvalidateOnAnimation(SubtitleCollapsingToolbarLayout.this);
- }
- }
-
- int getScrimAlpha() {
- return scrimAlpha;
- }
-
- /**
- * Set the drawable to use for the content scrim from resources. Providing null will disable the
- * scrim functionality.
- *
- * @param drawable the drawable to display
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrim(@Nullable Drawable drawable) {
- if (contentScrim != drawable) {
- if (contentScrim != null) {
- contentScrim.setCallback(null);
- }
- contentScrim = drawable != null ? drawable.mutate() : null;
- if (contentScrim != null) {
- contentScrim.setBounds(0, 0, getWidth(), getHeight());
- contentScrim.setCallback(this);
- contentScrim.setAlpha(scrimAlpha);
- }
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- /**
- * Set the color to use for the content scrim.
- *
- * @param color the color to display
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrimColor(@ColorInt int color) {
- setContentScrim(new ColorDrawable(color));
- }
-
- /**
- * Set the drawable to use for the content scrim from resources.
- *
- * @param resId drawable resource id
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrimResource(@DrawableRes int resId) {
- setContentScrim(ContextCompat.getDrawable(getContext(), resId));
- }
-
- /**
- * Returns the drawable which is used for the foreground scrim.
- *
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_contentScrim
- * @see #setContentScrim(Drawable)
- */
- @Nullable
- public Drawable getContentScrim() {
- return contentScrim;
- }
-
- /**
- * Set the drawable to use for the status bar scrim from resources. Providing null will disable
- * the scrim functionality.
- *
- *
This scrim is only shown when we have been given a top system inset.
- *
- * @param drawable the drawable to display
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrim(@Nullable Drawable drawable) {
- if (statusBarScrim != drawable) {
- if (statusBarScrim != null) {
- statusBarScrim.setCallback(null);
- }
- statusBarScrim = drawable != null ? drawable.mutate() : null;
- if (statusBarScrim != null) {
- if (statusBarScrim.isStateful()) {
- statusBarScrim.setState(getDrawableState());
- }
- DrawableCompat.setLayoutDirection(statusBarScrim, ViewCompat.getLayoutDirection(this));
- statusBarScrim.setVisible(getVisibility() == VISIBLE, false);
- statusBarScrim.setCallback(this);
- statusBarScrim.setAlpha(scrimAlpha);
- }
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- final int[] state = getDrawableState();
- boolean changed = false;
-
- Drawable d = statusBarScrim;
- if (d != null && d.isStateful()) {
- changed |= d.setState(state);
- }
- d = contentScrim;
- if (d != null && d.isStateful()) {
- changed |= d.setState(state);
- }
- if (collapsingTextHelper != null) {
- changed |= collapsingTextHelper.setState(state);
- }
-
- if (changed) {
- invalidate();
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || who == contentScrim || who == statusBarScrim;
- }
-
- @Override
- public void setVisibility(int visibility) {
- super.setVisibility(visibility);
-
- final boolean visible = visibility == VISIBLE;
- if (statusBarScrim != null && statusBarScrim.isVisible() != visible) {
- statusBarScrim.setVisible(visible, false);
- }
- if (contentScrim != null && contentScrim.isVisible() != visible) {
- contentScrim.setVisible(visible, false);
- }
- }
-
- /**
- * Set the color to use for the status bar scrim.
- *
- *
This scrim is only shown when we have been given a top system inset.
- *
- * @param color the color to display
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrimColor(@ColorInt int color) {
- setStatusBarScrim(new ColorDrawable(color));
- }
-
- /**
- * Set the drawable to use for the content scrim from resources.
- *
- * @param resId drawable resource id
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrimResource(@DrawableRes int resId) {
- setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId));
- }
-
- /**
- * Returns the drawable which is used for the status bar scrim.
- *
- * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_statusBarScrim
- * @see #setStatusBarScrim(Drawable)
- */
- @Nullable
- public Drawable getStatusBarScrim() {
- return statusBarScrim;
- }
-
- /**
- * Sets the text color and size for the collapsed title from the specified TextAppearance
- * resource.
- *
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_collapsedTitleTextAppearance
- */
- public void setCollapsedTitleTextAppearance(@StyleRes int resId) {
- collapsingTextHelper.setCollapsedTitleTextAppearance(resId);
- }
-
- /**
- * Sets the text color of the collapsed title.
- *
- * @param color The new text color in ARGB format
- */
- public void setCollapsedTitleTextColor(@ColorInt int color) {
- setCollapsedTitleTextColor(ColorStateList.valueOf(color));
- }
-
- /**
- * Sets the text colors of the collapsed title.
- *
- * @param colors ColorStateList containing the new text colors
- */
- public void setCollapsedTitleTextColor(@NonNull ColorStateList colors) {
- collapsingTextHelper.setCollapsedTitleTextColor(colors);
- }
-
- /**
- * Sets the text color and size for the collapsed subtitle from the specified TextAppearance
- * resource.
- *
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_collapsedSubtitleTextAppearance
- */
- public void setCollapsedSubtitleTextAppearance(@StyleRes int resId) {
- collapsingTextHelper.setCollapsedSubtitleTextAppearance(resId);
- }
-
- /**
- * Sets the text color of the collapsed subtitle.
- *
- * @param color The new text color in ARGB format
- */
- public void setCollapsedSubtitleTextColor(@ColorInt int color) {
- setCollapsedSubtitleTextColor(ColorStateList.valueOf(color));
- }
-
- /**
- * Sets the text colors of the collapsed subtitle.
- *
- * @param colors ColorStateList containing the new text colors
- */
- public void setCollapsedSubtitleTextColor(@NonNull ColorStateList colors) {
- collapsingTextHelper.setCollapsedSubtitleTextColor(colors);
- }
-
- /**
- * Sets the horizontal alignment of the collapsed title and the vertical gravity that will be used
- * when there is extra space in the collapsed bounds beyond what is required for the title itself.
- *
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_collapsedTitleGravity
- */
- public void setCollapsedTitleGravity(int gravity) {
- collapsingTextHelper.setCollapsedTextGravity(gravity);
- }
-
- /**
- * Returns the horizontal and vertical alignment for title when collapsed.
- *
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_collapsedTitleGravity
- */
- public int getCollapsedTitleGravity() {
- return collapsingTextHelper.getCollapsedTextGravity();
- }
-
- /**
- * Sets the text color and size for the expanded title from the specified TextAppearance resource.
- *
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleTextAppearance
- */
- public void setExpandedTitleTextAppearance(@StyleRes int resId) {
- collapsingTextHelper.setExpandedTitleTextAppearance(resId);
- }
-
- /**
- * Sets the text color of the expanded title.
- *
- * @param color The new text color in ARGB format
- */
- public void setExpandedTitleTextColor(@ColorInt int color) {
- setExpandedTitleTextColor(ColorStateList.valueOf(color));
- }
-
- /**
- * Sets the text colors of the expanded title.
- *
- * @param colors ColorStateList containing the new text colors
- */
- public void setExpandedTitleTextColor(@NonNull ColorStateList colors) {
- collapsingTextHelper.setExpandedTitleTextColor(colors);
- }
-
- /**
- * Sets the text color and size for the expanded subtitle from the specified TextAppearance resource.
- *
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedSubtitleTextAppearance
- */
- public void setExpandedSubtitleTextAppearance(@StyleRes int resId) {
- collapsingTextHelper.setExpandedSubtitleTextAppearance(resId);
- }
-
- /**
- * Sets the text color of the expanded subtitle.
- *
- * @param color The new text color in ARGB format
- */
- public void setExpandedSubtitleTextColor(@ColorInt int color) {
- setExpandedSubtitleTextColor(ColorStateList.valueOf(color));
- }
-
- /**
- * Sets the text colors of the expanded subtitle.
- *
- * @param colors ColorStateList containing the new text colors
- */
- public void setExpandedSubtitleTextColor(@NonNull ColorStateList colors) {
- collapsingTextHelper.setExpandedSubtitleTextColor(colors);
- }
-
- /**
- * Sets the horizontal alignment of the expanded title and the vertical gravity that will be used
- * when there is extra space in the expanded bounds beyond what is required for the title itself.
- *
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleGravity
- */
- public void setExpandedTitleGravity(int gravity) {
- collapsingTextHelper.setExpandedTextGravity(gravity);
- }
-
- /**
- * Returns the horizontal and vertical alignment for title when expanded.
- *
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleGravity
- */
- public int getExpandedTitleGravity() {
- return collapsingTextHelper.getExpandedTextGravity();
- }
-
- /**
- * Set the typeface to use for the collapsed title.
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setCollapsedTitleTypeface(@Nullable Typeface typeface) {
- collapsingTextHelper.setCollapsedTitleTypeface(typeface);
- }
-
- /**
- * Returns the typeface used for the collapsed title.
- */
- @NonNull
- public Typeface getCollapsedTitleTypeface() {
- return collapsingTextHelper.getCollapsedTitleTypeface();
- }
-
- /**
- * Set the typeface to use for the expanded title.
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setExpandedTitleTypeface(@Nullable Typeface typeface) {
- collapsingTextHelper.setExpandedTitleTypeface(typeface);
- }
-
- /**
- * Returns the typeface used for the expanded title.
- */
- @NonNull
- public Typeface getExpandedTitleTypeface() {
- return collapsingTextHelper.getExpandedTitleTypeface();
- }
-
- /**
- * Set the typeface to use for the collapsed title.
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setCollapsedSubtitleTypeface(@Nullable Typeface typeface) {
- collapsingTextHelper.setCollapsedSubtitleTypeface(typeface);
- }
-
- /**
- * Returns the typeface used for the collapsed title.
- */
- @NonNull
- public Typeface getCollapsedSubtitleTypeface() {
- return collapsingTextHelper.getCollapsedSubtitleTypeface();
- }
-
- /**
- * Set the typeface to use for the expanded title.
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setExpandedSubtitleTypeface(@Nullable Typeface typeface) {
- collapsingTextHelper.setExpandedSubtitleTypeface(typeface);
- }
-
- /**
- * Returns the typeface used for the expanded title.
- */
- @NonNull
- public Typeface getExpandedSubtitleTypeface() {
- return collapsingTextHelper.getExpandedSubtitleTypeface();
- }
-
- /**
- * Sets the expanded title margins.
- *
- * @param start the starting title margin in pixels
- * @param top the top title margin in pixels
- * @param end the ending title margin in pixels
- * @param bottom the bottom title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMargin
- * @see #getExpandedTitleMarginStart()
- * @see #getExpandedTitleMarginTop()
- * @see #getExpandedTitleMarginEnd()
- * @see #getExpandedTitleMarginBottom()
- */
- public void setExpandedTitleMargin(int start, int top, int end, int bottom) {
- expandedMarginStart = start;
- expandedMarginTop = top;
- expandedMarginEnd = end;
- expandedMarginBottom = bottom;
- requestLayout();
- }
-
- /**
- * @return the starting expanded title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginStart
- * @see #setExpandedTitleMarginStart(int)
- */
- public int getExpandedTitleMarginStart() {
- return expandedMarginStart;
- }
-
- /**
- * Sets the starting expanded title margin in pixels.
- *
- * @param margin the starting title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginStart
- * @see #getExpandedTitleMarginStart()
- */
- public void setExpandedTitleMarginStart(int margin) {
- expandedMarginStart = margin;
- requestLayout();
- }
-
- /**
- * @return the top expanded title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginTop
- * @see #setExpandedTitleMarginTop(int)
- */
- public int getExpandedTitleMarginTop() {
- return expandedMarginTop;
- }
-
- /**
- * Sets the top expanded title margin in pixels.
- *
- * @param margin the top title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginTop
- * @see #getExpandedTitleMarginTop()
- */
- public void setExpandedTitleMarginTop(int margin) {
- expandedMarginTop = margin;
- requestLayout();
- }
-
- /**
- * @return the ending expanded title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd
- * @see #setExpandedTitleMarginEnd(int)
- */
- public int getExpandedTitleMarginEnd() {
- return expandedMarginEnd;
- }
-
- /**
- * Sets the ending expanded title margin in pixels.
- *
- * @param margin the ending title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd
- * @see #getExpandedTitleMarginEnd()
- */
- public void setExpandedTitleMarginEnd(int margin) {
- expandedMarginEnd = margin;
- requestLayout();
- }
-
- /**
- * @return the bottom expanded title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginBottom
- * @see #setExpandedTitleMarginBottom(int)
- */
- public int getExpandedTitleMarginBottom() {
- return expandedMarginBottom;
- }
-
- /**
- * Sets the bottom expanded title margin in pixels.
- *
- * @param margin the bottom title margin in pixels
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginBottom
- * @see #getExpandedTitleMarginBottom()
- */
- public void setExpandedTitleMarginBottom(int margin) {
- expandedMarginBottom = margin;
- requestLayout();
- }
-
- /**
- * Sets whether {@code TextDirectionHeuristics} should be used to determine whether the title text
- * is RTL. Experimental Feature.
- */
- public void setRtlTextDirectionHeuristicsEnabled(boolean rtlTextDirectionHeuristicsEnabled) {
- collapsingTextHelper.setRtlTextDirectionHeuristicsEnabled(rtlTextDirectionHeuristicsEnabled);
- }
-
- /**
- * Gets whether {@code TextDirectionHeuristics} should be used to determine whether the title text
- * is RTL. Experimental Feature.
- */
- public boolean isRtlTextDirectionHeuristicsEnabled() {
- return collapsingTextHelper.isRtlTextDirectionHeuristicsEnabled();
- }
-
- /**
- * Set the amount of visible height in pixels used to define when to trigger a scrim visibility
- * change.
- *
- *
If the visible height of this view is less than the given value, the scrims will be made
- * visible, otherwise they are hidden.
- *
- * @param height value in pixels used to define when to trigger a scrim visibility change
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd
- */
- public void setScrimVisibleHeightTrigger(@IntRange(from = 0) final int height) {
- if (scrimVisibleHeightTrigger != height) {
- scrimVisibleHeightTrigger = height;
- // Update the scrim visibility
- updateScrimVisibility();
- }
- }
-
- /**
- * Returns the amount of visible height in pixels used to define when to trigger a scrim
- * visibility change.
- *
- * @see #setScrimVisibleHeightTrigger(int)
- */
- public int getScrimVisibleHeightTrigger() {
- if (scrimVisibleHeightTrigger >= 0) {
- // If we have one explicitly set, return it
- return scrimVisibleHeightTrigger;
- }
-
- // Otherwise we'll use the default computed value
- final int insetTop = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0;
-
- final int minHeight = ViewCompat.getMinimumHeight(this);
- if (minHeight > 0) {
- // If we have a minHeight set, lets use 2 * minHeight (capped at our height)
- return Math.min((minHeight * 2) + insetTop, getHeight());
- }
-
- // If we reach here then we don't have a min height set. Instead we'll take a
- // guess at 1/3 of our height being visible
- return getHeight() / 3;
- }
-
- /**
- * Set the duration used for scrim visibility animations.
- *
- * @param duration the duration to use in milliseconds
- * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_scrimAnimationDuration
- */
- public void setScrimAnimationDuration(@IntRange(from = 0) final long duration) {
- scrimAnimationDuration = duration;
- }
-
- /**
- * Returns the duration in milliseconds used for scrim visibility animations.
- */
- public long getScrimAnimationDuration() {
- return scrimAnimationDuration;
- }
-
- @Override
- protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
- return p instanceof LayoutParams;
- }
-
- @Override
- protected LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- }
-
- @Override
- public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
-
- @Override
- protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
- return new LayoutParams(p);
- }
-
- public static class LayoutParams extends CollapsingToolbarLayout.LayoutParams {
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
- }
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
-
- public LayoutParams(int width, int height, int gravity) {
- super(width, height, gravity);
- }
-
- public LayoutParams(ViewGroup.LayoutParams p) {
- super(p);
- }
-
- public LayoutParams(MarginLayoutParams source) {
- super(source);
- }
-
- @RequiresApi(19)
- public LayoutParams(FrameLayout.LayoutParams source) {
- super(source);
- }
- }
-
- /**
- * Show or hide the scrims if needed
- */
- final void updateScrimVisibility() {
- if (contentScrim != null || statusBarScrim != null) {
- setScrimsShown(getHeight() + currentOffset < getScrimVisibleHeightTrigger());
- }
- }
-
- final int getMaxOffsetForPinChild(View child) {
- final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- return getHeight() - offsetHelper.getLayoutTop() - child.getHeight() - lp.bottomMargin;
- }
-
- private void updateContentDescriptionFromTitle() {
- // Set this layout's contentDescription to match the title if it's shown by CollapsingTextHelper
- setContentDescription(getTitle());
- }
-
- private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
- OffsetUpdateListener() {
- }
-
- @Override
- public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
- currentOffset = verticalOffset;
-
- final int insetTop = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0;
-
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
-
- switch (lp.collapseMode) {
- case LayoutParams.COLLAPSE_MODE_PIN:
- offsetHelper.setTopAndBottomOffset(
- MathUtils.clamp(-verticalOffset, 0, getMaxOffsetForPinChild(child)));
- break;
- case LayoutParams.COLLAPSE_MODE_PARALLAX:
- offsetHelper.setTopAndBottomOffset(Math.round(-verticalOffset * lp.parallaxMult));
- break;
- default:
- break;
- }
- }
-
- // Show or hide the scrims if needed
- updateScrimVisibility();
-
- if (statusBarScrim != null && insetTop > 0) {
- ViewCompat.postInvalidateOnAnimation(SubtitleCollapsingToolbarLayout.this);
- }
-
- // Update the collapsing text's fraction
- final int expandRange = getHeight()
- - ViewCompat.getMinimumHeight(SubtitleCollapsingToolbarLayout.this)
- - insetTop;
- collapsingTextHelper.setExpansionFraction(Math.abs(verticalOffset) / (float) expandRange);
- }
- }
-}
diff --git a/app/src/main/java/com/google/android/material/internal/SubtitleCollapsingTextHelper.java b/app/src/main/java/com/google/android/material/internal/SubtitleCollapsingTextHelper.java
deleted file mode 100644
index 95848058c..000000000
--- a/app/src/main/java/com/google/android/material/internal/SubtitleCollapsingTextHelper.java
+++ /dev/null
@@ -1,1255 +0,0 @@
-package com.google.android.material.internal;
-
-import android.animation.TimeInterpolator;
-import android.content.res.ColorStateList;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.view.Gravity;
-import android.view.View;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.math.MathUtils;
-import androidx.core.text.TextDirectionHeuristicsCompat;
-import androidx.core.view.GravityCompat;
-import androidx.core.view.ViewCompat;
-
-import com.google.android.material.animation.AnimationUtils;
-import com.google.android.material.resources.CancelableFontCallback;
-import com.google.android.material.resources.TextAppearance;
-
-/**
- * Helper class for {@link com.google.android.material.appbar.SubtitleCollapsingToolbarLayout}.
- *
- * @see CollapsingTextHelper
- */
-public final class SubtitleCollapsingTextHelper {
-
- // Pre-JB-MR2 doesn't support HW accelerated canvas scaled title so we will workaround it
- // by using our own texture
- private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;
-
- private static final boolean DEBUG_DRAW = false;
- @NonNull
- private static final Paint DEBUG_DRAW_PAINT;
-
- static {
- DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
- if (DEBUG_DRAW_PAINT != null) {
- DEBUG_DRAW_PAINT.setAntiAlias(true);
- DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
- }
- }
-
- private final View view;
-
- private boolean drawTitle;
- private float expandedFraction;
-
- @NonNull
- private final Rect expandedBounds;
- @NonNull
- private final Rect collapsedBounds;
- @NonNull
- private final RectF currentBounds;
- private int expandedTextGravity = Gravity.CENTER_VERTICAL;
- private int collapsedTextGravity = Gravity.CENTER_VERTICAL;
- private float expandedTitleTextSize, expandedSubtitleTextSize = 15;
- private float collapsedTitleTextSize, collapsedSubtitleTextSize = 15;
- private ColorStateList expandedTitleTextColor, expandedSubtitleTextColor;
- private ColorStateList collapsedTitleTextColor, collapsedSubtitleTextColor;
-
- private float expandedTitleDrawY, expandedSubtitleDrawY;
- private float collapsedTitleDrawY, collapsedSubtitleDrawY;
- private float expandedTitleDrawX, expandedSubtitleDrawX;
- private float collapsedTitleDrawX, collapsedSubtitleDrawX;
- private float currentTitleDrawX, currentSubtitleDrawX;
- private float currentTitleDrawY, currentSubtitleDrawY;
- private Typeface collapsedTitleTypeface, collapsedSubtitleTypeface;
- private Typeface expandedTitleTypeface, expandedSubtitleTypeface;
- private Typeface currentTitleTypeface, currentSubtitleTypeface;
- private CancelableFontCallback expandedTitleFontCallback, expandedSubtitleFontCallback;
- private CancelableFontCallback collapsedTitleFontCallback, collapsedSubtitleFontCallback;
-
- @Nullable
- private CharSequence title, subtitle;
- @Nullable
- private CharSequence titleToDraw, subtitleToDraw;
- private boolean isRtl;
- private boolean isRtlTextDirectionHeuristicsEnabled = true;
-
- private boolean useTexture;
- @Nullable
- private Bitmap expandedTitleTexture, expandedSubtitleTexture;
- private Paint titleTexturePaint, subtitleTexturePaint;
- private float titleTextureAscent, subtitleTextureAscent;
- private float titleTextureDescent, subtitleTextureDescent;
-
- private float titleScale, subtitleScale;
- private float currentTitleTextSize, currentSubtitleTextSize;
-
- private int[] state;
-
- private boolean boundsChanged;
-
- @NonNull
- private final TextPaint titleTextPaint, subtitleTextPaint;
- @NonNull
- private final TextPaint titleTmpPaint, subtitleTmpPaint;
-
- private TimeInterpolator positionInterpolator;
- private TimeInterpolator textSizeInterpolator;
-
- private float collapsedTitleShadowRadius, collapsedSubtitleShadowRadius;
- private float collapsedTitleShadowDx, collapsedSubtitleShadowDx;
- private float collapsedTitleShadowDy, collapsedSubtitleShadowDy;
- private ColorStateList collapsedTitleShadowColor, collapsedSubtitleShadowColor;
-
- private float expandedTitleShadowRadius, expandedSubtitleShadowRadius;
- private float expandedTitleShadowDx, expandedSubtitleShadowDx;
- private float expandedTitleShadowDy, expandedSubtitleShadowDy;
- private ColorStateList expandedTitleShadowColor, expandedSubtitleShadowColor;
-
- public SubtitleCollapsingTextHelper(View view) {
- this.view = view;
-
- titleTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
- titleTmpPaint = new TextPaint(titleTextPaint);
- subtitleTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
- subtitleTmpPaint = new TextPaint(subtitleTextPaint);
-
- collapsedBounds = new Rect();
- expandedBounds = new Rect();
- currentBounds = new RectF();
- }
-
-
- public void setTextSizeInterpolator(TimeInterpolator interpolator) {
- textSizeInterpolator = interpolator;
- recalculate();
- }
-
- public void setPositionInterpolator(TimeInterpolator interpolator) {
- positionInterpolator = interpolator;
- recalculate();
- }
-
- public void setExpandedTitleTextSize(float textSize) {
- if (expandedTitleTextSize != textSize) {
- expandedTitleTextSize = textSize;
- recalculate();
- }
- }
-
- public void setCollapsedTitleTextSize(float textSize) {
- if (collapsedTitleTextSize != textSize) {
- collapsedTitleTextSize = textSize;
- recalculate();
- }
- }
-
- public void setExpandedSubtitleTextSize(float textSize) {
- if (expandedSubtitleTextSize != textSize) {
- expandedSubtitleTextSize = textSize;
- recalculate();
- }
- }
-
- public void setCollapsedSubtitleTextSize(float textSize) {
- if (collapsedSubtitleTextSize != textSize) {
- collapsedSubtitleTextSize = textSize;
- recalculate();
- }
- }
-
- public void setCollapsedTitleTextColor(ColorStateList textColor) {
- if (collapsedTitleTextColor != textColor) {
- collapsedTitleTextColor = textColor;
- recalculate();
- }
- }
-
- public void setExpandedTitleTextColor(ColorStateList textColor) {
- if (expandedTitleTextColor != textColor) {
- expandedTitleTextColor = textColor;
- recalculate();
- }
- }
-
- public void setCollapsedSubtitleTextColor(ColorStateList textColor) {
- if (collapsedSubtitleTextColor != textColor) {
- collapsedSubtitleTextColor = textColor;
- recalculate();
- }
- }
-
- public void setExpandedSubtitleTextColor(ColorStateList textColor) {
- if (expandedSubtitleTextColor != textColor) {
- expandedSubtitleTextColor = textColor;
- recalculate();
- }
- }
-
- public void setExpandedBounds(int left, int top, int right, int bottom) {
- if (!rectEquals(expandedBounds, left, top, right, bottom)) {
- expandedBounds.set(left, top, right, bottom);
- boundsChanged = true;
- onBoundsChanged();
- }
- }
-
- public void setExpandedBounds(@NonNull Rect bounds) {
- setExpandedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
- }
-
- public void setCollapsedBounds(int left, int top, int right, int bottom) {
- if (!rectEquals(collapsedBounds, left, top, right, bottom)) {
- collapsedBounds.set(left, top, right, bottom);
- boundsChanged = true;
- onBoundsChanged();
- }
- }
-
- public void setCollapsedBounds(@NonNull Rect bounds) {
- setCollapsedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
- }
-
- public void getCollapsedTitleTextActualBounds(@NonNull RectF bounds) {
- boolean isRtl = calculateIsRtl(title);
-
- bounds.left = !isRtl ? collapsedBounds.left : collapsedBounds.right - calculateCollapsedTitleTextWidth();
- bounds.top = collapsedBounds.top;
- bounds.right = !isRtl ? bounds.left + calculateCollapsedTitleTextWidth() : collapsedBounds.right;
- bounds.bottom = collapsedBounds.top + getCollapsedTitleTextHeight();
- }
-
- public float calculateCollapsedTitleTextWidth() {
- if (title == null) {
- return 0;
- }
- getTitleTextPaintCollapsed(titleTmpPaint);
- return titleTmpPaint.measureText(title, 0, title.length());
- }
-
- public void getCollapsedSubtitleTextActualBounds(@NonNull RectF bounds) {
- boolean isRtl = calculateIsRtl(subtitle);
-
- bounds.left = !isRtl ? collapsedBounds.left : collapsedBounds.right - calculateCollapsedSubtitleTextWidth();
- bounds.top = collapsedBounds.top;
- bounds.right = !isRtl ? bounds.left + calculateCollapsedSubtitleTextWidth() : collapsedBounds.right;
- bounds.bottom = collapsedBounds.top + getCollapsedSubtitleTextHeight();
- }
-
- public float calculateCollapsedSubtitleTextWidth() {
- if (subtitle == null) {
- return 0;
- }
- getSubtitleTextPaintCollapsed(subtitleTmpPaint);
- return subtitleTmpPaint.measureText(subtitle, 0, subtitle.length());
- }
-
- public float getExpandedTitleTextHeight() {
- getTitleTextPaintExpanded(titleTmpPaint);
- // Return expanded height measured from the baseline.
- return -titleTmpPaint.ascent();
- }
-
- public float getCollapsedTitleTextHeight() {
- getTitleTextPaintCollapsed(titleTmpPaint);
- // Return collapsed height measured from the baseline.
- return -titleTmpPaint.ascent();
- }
-
- public float getExpandedSubtitleTextHeight() {
- getSubtitleTextPaintExpanded(subtitleTmpPaint);
- // Return expanded height measured from the baseline.
- return -subtitleTmpPaint.ascent();
- }
-
- public float getCollapsedSubtitleTextHeight() {
- getSubtitleTextPaintCollapsed(subtitleTmpPaint);
- // Return collapsed height measured from the baseline.
- return -subtitleTmpPaint.ascent();
- }
-
- private void getTitleTextPaintExpanded(@NonNull TextPaint textPaint) {
- textPaint.setTextSize(expandedTitleTextSize);
- textPaint.setTypeface(expandedTitleTypeface);
- }
-
- private void getTitleTextPaintCollapsed(@NonNull TextPaint textPaint) {
- textPaint.setTextSize(collapsedTitleTextSize);
- textPaint.setTypeface(collapsedTitleTypeface);
- }
-
- private void getSubtitleTextPaintExpanded(@NonNull TextPaint textPaint) {
- textPaint.setTextSize(expandedSubtitleTextSize);
- textPaint.setTypeface(expandedSubtitleTypeface);
- }
-
- private void getSubtitleTextPaintCollapsed(@NonNull TextPaint textPaint) {
- textPaint.setTextSize(collapsedSubtitleTextSize);
- textPaint.setTypeface(collapsedSubtitleTypeface);
- }
-
- void onBoundsChanged() {
- drawTitle = collapsedBounds.width() > 0
- && collapsedBounds.height() > 0
- && expandedBounds.width() > 0
- && expandedBounds.height() > 0;
- }
-
- public void setExpandedTextGravity(int gravity) {
- if (expandedTextGravity != gravity) {
- expandedTextGravity = gravity;
- recalculate();
- }
- }
-
- public int getExpandedTextGravity() {
- return expandedTextGravity;
- }
-
- public void setCollapsedTextGravity(int gravity) {
- if (collapsedTextGravity != gravity) {
- collapsedTextGravity = gravity;
- recalculate();
- }
- }
-
- public int getCollapsedTextGravity() {
- return collapsedTextGravity;
- }
-
- public void setCollapsedTitleTextAppearance(int resId) {
- TextAppearance textAppearance = new TextAppearance(view.getContext(), resId);
-
- if (textAppearance.getTextColor() != null) {
- collapsedTitleTextColor = textAppearance.getTextColor();
- }
- if (textAppearance.getTextSize() != 0) {
- collapsedTitleTextSize = textAppearance.getTextSize();
- }
- if (textAppearance.shadowColor != null) {
- collapsedTitleShadowColor = textAppearance.shadowColor;
- }
- collapsedTitleShadowDx = textAppearance.shadowDx;
- collapsedTitleShadowDy = textAppearance.shadowDy;
- collapsedTitleShadowRadius = textAppearance.shadowRadius;
-
- // Cancel pending async fetch, if any, and replace with a new one.
- if (collapsedTitleFontCallback != null) {
- collapsedTitleFontCallback.cancel();
- }
- collapsedTitleFontCallback = new CancelableFontCallback(new CancelableFontCallback.ApplyFont() {
- @Override
- public void apply(Typeface font) {
- setCollapsedTitleTypeface(font);
- }
- }, textAppearance.getFallbackFont());
- textAppearance.getFontAsync(view.getContext(), collapsedTitleFontCallback);
-
- recalculate();
- }
-
- public void setExpandedTitleTextAppearance(int resId) {
- TextAppearance textAppearance = new TextAppearance(view.getContext(), resId);
- if (textAppearance.getTextColor() != null) {
- expandedTitleTextColor = textAppearance.getTextColor();
- }
- if (textAppearance.getTextSize() != 0) {
- expandedTitleTextSize = textAppearance.getTextSize();
- }
- if (textAppearance.shadowColor != null) {
- expandedTitleShadowColor = textAppearance.shadowColor;
- }
- expandedTitleShadowDx = textAppearance.shadowDx;
- expandedTitleShadowDy = textAppearance.shadowDy;
- expandedTitleShadowRadius = textAppearance.shadowRadius;
-
- // Cancel pending async fetch, if any, and replace with a new one.
- if (expandedTitleFontCallback != null) {
- expandedTitleFontCallback.cancel();
- }
- expandedTitleFontCallback = new CancelableFontCallback(new CancelableFontCallback.ApplyFont() {
- @Override
- public void apply(Typeface font) {
- setExpandedTitleTypeface(font);
- }
- }, textAppearance.getFallbackFont());
- textAppearance.getFontAsync(view.getContext(), expandedTitleFontCallback);
-
- recalculate();
- }
-
- public void setCollapsedSubtitleTextAppearance(int resId) {
- TextAppearance textAppearance = new TextAppearance(view.getContext(), resId);
-
- if (textAppearance.getTextColor() != null) {
- collapsedSubtitleTextColor = textAppearance.getTextColor();
- }
- if (textAppearance.getTextSize() != 0) {
- collapsedSubtitleTextSize = textAppearance.getTextSize();
- }
- if (textAppearance.shadowColor != null) {
- collapsedSubtitleShadowColor = textAppearance.shadowColor;
- }
- collapsedSubtitleShadowDx = textAppearance.shadowDx;
- collapsedSubtitleShadowDy = textAppearance.shadowDy;
- collapsedSubtitleShadowRadius = textAppearance.shadowRadius;
-
- // Cancel pending async fetch, if any, and replace with a new one.
- if (collapsedSubtitleFontCallback != null) {
- collapsedSubtitleFontCallback.cancel();
- }
- collapsedSubtitleFontCallback = new CancelableFontCallback(new CancelableFontCallback.ApplyFont() {
- @Override
- public void apply(Typeface font) {
- setCollapsedSubtitleTypeface(font);
- }
- }, textAppearance.getFallbackFont());
- textAppearance.getFontAsync(view.getContext(), collapsedSubtitleFontCallback);
-
- recalculate();
- }
-
- public void setExpandedSubtitleTextAppearance(int resId) {
- TextAppearance textAppearance = new TextAppearance(view.getContext(), resId);
- if (textAppearance.getTextColor() != null) {
- expandedSubtitleTextColor = textAppearance.getTextColor();
- }
- if (textAppearance.getTextSize() != 0) {
- expandedSubtitleTextSize = textAppearance.getTextSize();
- }
- if (textAppearance.shadowColor != null) {
- expandedSubtitleShadowColor = textAppearance.shadowColor;
- }
- expandedSubtitleShadowDx = textAppearance.shadowDx;
- expandedSubtitleShadowDy = textAppearance.shadowDy;
- expandedSubtitleShadowRadius = textAppearance.shadowRadius;
-
- // Cancel pending async fetch, if any, and replace with a new one.
- if (expandedSubtitleFontCallback != null) {
- expandedSubtitleFontCallback.cancel();
- }
- expandedSubtitleFontCallback = new CancelableFontCallback(new CancelableFontCallback.ApplyFont() {
- @Override
- public void apply(Typeface font) {
- if (font != null) setExpandedSubtitleTypeface(font);
- }
- }, null);
- textAppearance.getFontAsync(view.getContext(), expandedSubtitleFontCallback);
-
- recalculate();
- }
-
- public void setCollapsedTitleTypeface(Typeface typeface) {
- if (setCollapsedTitleTypefaceInternal(typeface)) {
- recalculate();
- }
- }
-
- public void setExpandedTitleTypeface(Typeface typeface) {
- if (setExpandedTitleTypefaceInternal(typeface)) {
- recalculate();
- }
- }
-
- public void setCollapsedSubtitleTypeface(Typeface typeface) {
- if (setCollapsedSubtitleTypefaceInternal(typeface)) {
- recalculate();
- }
- }
-
- public void setExpandedSubtitleTypeface(Typeface typeface) {
- if (setExpandedSubtitleTypefaceInternal(typeface)) {
- recalculate();
- }
- }
-
- public void setTitleTypefaces(Typeface typeface) {
- boolean collapsedFontChanged = setCollapsedTitleTypefaceInternal(typeface);
- boolean expandedFontChanged = setExpandedTitleTypefaceInternal(typeface);
- if (collapsedFontChanged || expandedFontChanged) {
- recalculate();
- }
- }
-
- public void setSubtitleTypefaces(Typeface typeface) {
- boolean collapsedFontChanged = setCollapsedSubtitleTypefaceInternal(typeface);
- boolean expandedFontChanged = setExpandedSubtitleTypefaceInternal(typeface);
- if (collapsedFontChanged || expandedFontChanged) {
- recalculate();
- }
- }
-
- @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
- private boolean setCollapsedTitleTypefaceInternal(Typeface typeface) {
- // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding
- // already updated one when async op comes back after a while.
- if (collapsedTitleFontCallback != null) {
- collapsedTitleFontCallback.cancel();
- }
- if (collapsedTitleTypeface != typeface) {
- collapsedTitleTypeface = typeface;
- return true;
- }
- return false;
- }
-
- @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
- private boolean setExpandedTitleTypefaceInternal(Typeface typeface) {
- // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding
- // already updated one when async op comes back after a while.
- if (expandedTitleFontCallback != null) {
- expandedTitleFontCallback.cancel();
- }
- if (expandedTitleTypeface != typeface) {
- expandedTitleTypeface = typeface;
- return true;
- }
- return false;
- }
-
- @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
- private boolean setCollapsedSubtitleTypefaceInternal(Typeface typeface) {
- // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding
- // already updated one when async op comes back after a while.
- if (collapsedSubtitleFontCallback != null) {
- collapsedSubtitleFontCallback.cancel();
- }
- if (collapsedSubtitleTypeface != typeface) {
- collapsedSubtitleTypeface = typeface;
- return true;
- }
- return false;
- }
-
- @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
- private boolean setExpandedSubtitleTypefaceInternal(Typeface typeface) {
- // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding
- // already updated one when async op comes back after a while.
- if (expandedSubtitleFontCallback != null) {
- expandedSubtitleFontCallback.cancel();
- }
- if (expandedSubtitleTypeface != typeface) {
- expandedSubtitleTypeface = typeface;
- return true;
- }
- return false;
- }
-
- public Typeface getCollapsedTitleTypeface() {
- return collapsedTitleTypeface != null ? collapsedTitleTypeface : Typeface.DEFAULT;
- }
-
- public Typeface getExpandedTitleTypeface() {
- return expandedTitleTypeface != null ? expandedTitleTypeface : Typeface.DEFAULT;
- }
-
- public Typeface getCollapsedSubtitleTypeface() {
- return collapsedSubtitleTypeface != null ? collapsedSubtitleTypeface : Typeface.DEFAULT;
- }
-
- public Typeface getExpandedSubtitleTypeface() {
- return expandedSubtitleTypeface != null ? expandedSubtitleTypeface : Typeface.DEFAULT;
- }
-
- /**
- * Set the value indicating the current scroll value. This decides how much of the background will
- * be displayed, as well as the title metrics/positioning.
- *
- *
A value of {@code 0.0} indicates that the layout is fully expanded. A value of {@code 1.0}
- * indicates that the layout is fully collapsed.
- */
- public void setExpansionFraction(float fraction) {
- fraction = MathUtils.clamp(fraction, 0f, 1f);
-
- if (fraction != expandedFraction) {
- expandedFraction = fraction;
- calculateCurrentOffsets();
- }
- }
-
- public final boolean setState(final int[] state) {
- this.state = state;
-
- if (isStateful()) {
- recalculate();
- return true;
- }
-
- return false;
- }
-
- public final boolean isStateful() {
- return (collapsedTitleTextColor != null && collapsedTitleTextColor.isStateful())
- || (expandedTitleTextColor != null && expandedTitleTextColor.isStateful());
- }
-
- public float getExpansionFraction() {
- return expandedFraction;
- }
-
- public float getCollapsedTitleTextSize() {
- return collapsedTitleTextSize;
- }
-
- public float getExpandedTitleTextSize() {
- return expandedTitleTextSize;
- }
-
- public float getCollapsedSubtitleTextSize() {
- return collapsedSubtitleTextSize;
- }
-
- public float getExpandedSubtitleTextSize() {
- return expandedSubtitleTextSize;
- }
-
- public void setRtlTextDirectionHeuristicsEnabled(boolean rtlTextDirectionHeuristicsEnabled) {
- isRtlTextDirectionHeuristicsEnabled = rtlTextDirectionHeuristicsEnabled;
- }
-
- public boolean isRtlTextDirectionHeuristicsEnabled() {
- return isRtlTextDirectionHeuristicsEnabled;
- }
-
- private void calculateCurrentOffsets() {
- calculateOffsets(expandedFraction);
- }
-
- private void calculateOffsets(final float fraction) {
- interpolateBounds(fraction);
- currentTitleDrawX = lerp(expandedTitleDrawX, collapsedTitleDrawX, fraction, positionInterpolator);
- currentTitleDrawY = lerp(expandedTitleDrawY, collapsedTitleDrawY, fraction, positionInterpolator);
- currentSubtitleDrawX = lerp(expandedSubtitleDrawX, collapsedSubtitleDrawX, fraction, positionInterpolator);
- currentSubtitleDrawY = lerp(expandedSubtitleDrawY, collapsedSubtitleDrawY, fraction, positionInterpolator);
-
- setInterpolatedTitleTextSize(lerp(expandedTitleTextSize, collapsedTitleTextSize, fraction, textSizeInterpolator));
- setInterpolatedSubtitleTextSize(lerp(expandedSubtitleTextSize, collapsedSubtitleTextSize, fraction, textSizeInterpolator));
-
- if (collapsedTitleTextColor != expandedTitleTextColor) {
- // If the collapsed and expanded title colors are different, blend them based on the
- // fraction
- titleTextPaint.setColor(blendColors(getCurrentExpandedTitleTextColor(), getCurrentCollapsedTitleTextColor(), fraction));
- } else {
- titleTextPaint.setColor(getCurrentCollapsedTitleTextColor());
- }
-
- titleTextPaint.setShadowLayer(
- lerp(expandedTitleShadowRadius, collapsedTitleShadowRadius, fraction, null),
- lerp(expandedTitleShadowDx, collapsedTitleShadowDx, fraction, null),
- lerp(expandedTitleShadowDy, collapsedTitleShadowDy, fraction, null),
- blendColors(getCurrentColor(expandedTitleShadowColor), getCurrentColor(collapsedTitleShadowColor), fraction));
-
- if (collapsedSubtitleTextColor != expandedSubtitleTextColor) {
- // If the collapsed and expanded title colors are different, blend them based on the
- // fraction
- subtitleTextPaint.setColor(blendColors(getCurrentExpandedSubtitleTextColor(), getCurrentCollapsedSubtitleTextColor(), fraction));
- } else {
- subtitleTextPaint.setColor(getCurrentCollapsedSubtitleTextColor());
- }
-
- subtitleTextPaint.setShadowLayer(
- lerp(expandedSubtitleShadowRadius, collapsedSubtitleShadowRadius, fraction, null),
- lerp(expandedSubtitleShadowDx, collapsedSubtitleShadowDx, fraction, null),
- lerp(expandedSubtitleShadowDy, collapsedSubtitleShadowDy, fraction, null),
- blendColors(getCurrentColor(expandedSubtitleShadowColor), getCurrentColor(collapsedSubtitleShadowColor), fraction));
-
- ViewCompat.postInvalidateOnAnimation(view);
- }
-
- @ColorInt
- private int getCurrentExpandedTitleTextColor() {
- return getCurrentColor(expandedTitleTextColor);
- }
-
- @ColorInt
- private int getCurrentExpandedSubtitleTextColor() {
- return getCurrentColor(expandedSubtitleTextColor);
- }
-
- @ColorInt
- public int getCurrentCollapsedTitleTextColor() {
- return getCurrentColor(collapsedTitleTextColor);
- }
-
- @ColorInt
- public int getCurrentCollapsedSubtitleTextColor() {
- return getCurrentColor(collapsedSubtitleTextColor);
- }
-
- @ColorInt
- private int getCurrentColor(@Nullable ColorStateList colorStateList) {
- if (colorStateList == null) {
- return 0;
- }
- if (state != null) {
- return colorStateList.getColorForState(state, 0);
- }
- return colorStateList.getDefaultColor();
- }
-
- private void calculateBaseOffsets() {
- final float currentTitleSize = this.currentTitleTextSize;
- final float currentSubtitleSize = this.currentSubtitleTextSize;
- final boolean isTitleOnly = TextUtils.isEmpty(subtitle);
-
- // We then calculate the collapsed title size, using the same logic
- calculateUsingTitleTextSize(collapsedTitleTextSize);
- calculateUsingSubtitleTextSize(collapsedSubtitleTextSize);
- float titleWidth = titleToDraw != null ? titleTextPaint.measureText(titleToDraw, 0, titleToDraw.length()) : 0;
- float subtitleWidth = subtitleToDraw != null ? subtitleTextPaint.measureText(subtitleToDraw, 0, subtitleToDraw.length()) : 0;
- final int collapsedAbsGravity =
- GravityCompat.getAbsoluteGravity(
- collapsedTextGravity,
- isRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
-
- // reusable dimension
- float titleHeight = titleTextPaint.descent() - titleTextPaint.ascent();
- float titleOffset = titleHeight / 2 - titleTextPaint.descent();
- float subtitleHeight = subtitleTextPaint.descent() - subtitleTextPaint.ascent();
- float subtitleOffset = subtitleHeight / 2 - subtitleTextPaint.descent();
-
- if (isTitleOnly) {
- switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.BOTTOM:
- collapsedTitleDrawY = collapsedBounds.bottom;
- break;
- case Gravity.TOP:
- collapsedTitleDrawY = collapsedBounds.top - titleTextPaint.ascent();
- break;
- case Gravity.CENTER_VERTICAL:
- default:
- float textHeight = titleTextPaint.descent() - titleTextPaint.ascent();
- float textOffset = (textHeight / 2) - titleTextPaint.descent();
- collapsedTitleDrawY = collapsedBounds.centerY() + textOffset;
- break;
- }
- } else {
- final float offset = (collapsedBounds.height() - (titleHeight + subtitleHeight)) / 3;
- collapsedTitleDrawY = collapsedBounds.top + offset - titleTextPaint.ascent();
- collapsedSubtitleDrawY = collapsedBounds.top + offset * 2 + titleHeight - subtitleTextPaint.ascent();
- }
- switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
- case Gravity.CENTER_HORIZONTAL:
- collapsedTitleDrawX = collapsedBounds.centerX() - (titleWidth / 2);
- collapsedSubtitleDrawX = collapsedBounds.centerX() - (subtitleWidth / 2);
- break;
- case Gravity.RIGHT:
- collapsedTitleDrawX = collapsedBounds.right - titleWidth;
- collapsedSubtitleDrawX = collapsedBounds.right - subtitleWidth;
- break;
- case Gravity.LEFT:
- default:
- collapsedTitleDrawX = collapsedBounds.left;
- collapsedSubtitleDrawX = collapsedBounds.left;
- break;
- }
-
- calculateUsingTitleTextSize(expandedTitleTextSize);
- calculateUsingSubtitleTextSize(expandedSubtitleTextSize);
- titleWidth = titleToDraw != null ? titleTextPaint.measureText(titleToDraw, 0, titleToDraw.length()) : 0;
- subtitleWidth = subtitleToDraw != null ? subtitleTextPaint.measureText(subtitleToDraw, 0, subtitleToDraw.length()) : 0;
-
- // dimension modification
- titleHeight = titleTextPaint.descent() - titleTextPaint.ascent();
- titleOffset = titleHeight / 2 - titleTextPaint.descent();
- subtitleHeight = subtitleTextPaint.descent() - subtitleTextPaint.ascent();
- subtitleOffset = subtitleHeight / 2 - subtitleTextPaint.descent();
-
- final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(
- expandedTextGravity,
- isRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR
- );
- if (isTitleOnly) {
- switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.BOTTOM:
- expandedTitleDrawY = expandedBounds.bottom;
- break;
- case Gravity.TOP:
- expandedTitleDrawY = expandedBounds.top - titleTextPaint.ascent();
- break;
- case Gravity.CENTER_VERTICAL:
- default:
- float textHeight = titleTextPaint.descent() - titleTextPaint.ascent();
- float textOffset = (textHeight / 2) - titleTextPaint.descent();
- expandedTitleDrawY = expandedBounds.centerY() + textOffset;
- break;
- }
- } else {
- switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.BOTTOM:
- expandedTitleDrawY = expandedBounds.bottom - subtitleHeight - titleOffset;
- expandedSubtitleDrawY = expandedBounds.bottom;
- break;
- case Gravity.TOP:
- expandedTitleDrawY = expandedBounds.top - titleTextPaint.ascent();
- expandedSubtitleDrawY = expandedTitleDrawY + subtitleHeight + titleOffset;
- break;
- case Gravity.CENTER_VERTICAL:
- default:
- expandedTitleDrawY = expandedBounds.centerY() + titleOffset;
- expandedSubtitleDrawY = expandedTitleDrawY + subtitleHeight + titleOffset;
- break;
- }
- }
- switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
- case Gravity.CENTER_HORIZONTAL:
- expandedTitleDrawX = expandedBounds.centerX() - (titleWidth / 2);
- expandedSubtitleDrawX = expandedBounds.centerX() - (subtitleWidth / 2);
- break;
- case Gravity.RIGHT:
- expandedTitleDrawX = expandedBounds.right - titleWidth;
- expandedSubtitleDrawX = expandedBounds.right - subtitleWidth;
- break;
- case Gravity.LEFT:
- default:
- expandedTitleDrawX = expandedBounds.left;
- expandedSubtitleDrawX = expandedBounds.left;
- break;
- }
-
- // The bounds have changed so we need to clear the texture
- clearTexture();
- // Now reset the title size back to the original
- setInterpolatedTitleTextSize(currentTitleSize);
- setInterpolatedSubtitleTextSize(currentSubtitleSize);
- }
-
- private void interpolateBounds(float fraction) {
- currentBounds.left = lerp(expandedBounds.left, collapsedBounds.left, fraction, positionInterpolator);
- currentBounds.top = lerp(expandedTitleDrawY, collapsedTitleDrawY, fraction, positionInterpolator);
- currentBounds.right = lerp(expandedBounds.right, collapsedBounds.right, fraction, positionInterpolator);
- currentBounds.bottom = lerp(expandedBounds.bottom, collapsedBounds.bottom, fraction, positionInterpolator);
- }
-
- public void draw(@NonNull Canvas canvas) {
- final int saveCount = canvas.save();
-
- if (drawTitle && titleToDraw != null) {
- float titleX = currentTitleDrawX;
- float titleY = currentTitleDrawY;
- float subtitleX = currentSubtitleDrawX;
- float subtitleY = currentSubtitleDrawY;
-
- final boolean drawTitleTexture = useTexture && expandedTitleTexture != null;
- final boolean drawSubtitleTexture = useTexture && expandedSubtitleTexture != null;
-
- final float titleAscent;
- final float titleDescent;
- if (drawTitleTexture) {
- titleAscent = titleTextureAscent * titleScale;
- titleDescent = titleTextureDescent * titleScale;
- } else {
- titleAscent = titleTextPaint.ascent() * titleScale;
- titleDescent = titleTextPaint.descent() * titleScale;
- }
-
- if (DEBUG_DRAW) {
- // Just a debug tool, which drawn a magenta rect in the text bounds
- canvas.drawRect(currentBounds.left, titleY + titleAscent, currentBounds.right, titleY + titleDescent, DEBUG_DRAW_PAINT);
- }
-
- if (drawTitleTexture) {
- titleY += titleAscent;
- }
-
- // additional canvas save for subtitle
- if (subtitleToDraw != null) {
- final int subtitleSaveCount = canvas.save();
-
- if (subtitleScale != 1f) {
- canvas.scale(subtitleScale, subtitleScale, subtitleX, subtitleY);
- }
-
- if (drawSubtitleTexture) {
- // If we should use a texture, draw it instead of title
- canvas.drawBitmap(expandedSubtitleTexture, subtitleX, subtitleY, subtitleTexturePaint);
- } else {
- canvas.drawText(subtitleToDraw, 0, subtitleToDraw.length(), subtitleX, subtitleY, subtitleTextPaint);
- }
- canvas.restoreToCount(subtitleSaveCount);
- }
-
- if (titleScale != 1f) {
- canvas.scale(titleScale, titleScale, titleX, titleY);
- }
-
- if (drawTitleTexture) {
- // If we should use a texture, draw it instead of text
- canvas.drawBitmap(expandedTitleTexture, titleX, titleY, titleTexturePaint);
- } else {
- canvas.drawText(titleToDraw, 0, titleToDraw.length(), titleX, titleY, titleTextPaint);
- }
- }
-
- canvas.restoreToCount(saveCount);
- }
-
- private boolean calculateIsRtl(@NonNull CharSequence text) {
- final boolean defaultIsRtl = isDefaultIsRtl();
- return isRtlTextDirectionHeuristicsEnabled
- ? isTextDirectionHeuristicsIsRtl(text, defaultIsRtl)
- : defaultIsRtl;
- }
-
- private boolean isDefaultIsRtl() {
- return ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL;
- }
-
- private boolean isTextDirectionHeuristicsIsRtl(@NonNull CharSequence text, boolean defaultIsRtl) {
- return (defaultIsRtl
- ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL
- : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR)
- .isRtl(text, 0, text.length());
- }
-
- private void setInterpolatedTitleTextSize(float textSize) {
- calculateUsingTitleTextSize(textSize);
-
- // Use our texture if the scale isn't 1.0
- useTexture = USE_SCALING_TEXTURE && titleScale != 1f;
-
- if (useTexture) {
- // Make sure we have an expanded texture if needed
- ensureExpandedTitleTexture();
- }
-
- ViewCompat.postInvalidateOnAnimation(view);
- }
-
- private void setInterpolatedSubtitleTextSize(float textSize) {
- calculateUsingSubtitleTextSize(textSize);
-
- // Use our texture if the scale isn't 1.0
- useTexture = USE_SCALING_TEXTURE && subtitleScale != 1f;
-
- if (useTexture) {
- // Make sure we have an expanded texture if needed
- ensureExpandedSubtitleTexture();
- }
-
- ViewCompat.postInvalidateOnAnimation(view);
- }
-
- @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
- private void calculateUsingTitleTextSize(final float size) {
- if (title == null) {
- return;
- }
-
- final float collapsedWidth = collapsedBounds.width();
- final float expandedWidth = expandedBounds.width();
-
- final float availableWidth;
- final float newTextSize;
- boolean updateDrawText = false;
-
- if (isClose(size, collapsedTitleTextSize)) {
- newTextSize = collapsedTitleTextSize;
- titleScale = 1f;
- if (currentTitleTypeface != collapsedTitleTypeface) {
- currentTitleTypeface = collapsedTitleTypeface;
- updateDrawText = true;
- }
- availableWidth = collapsedWidth;
- } else {
- newTextSize = expandedTitleTextSize;
- if (currentTitleTypeface != expandedTitleTypeface) {
- currentTitleTypeface = expandedTitleTypeface;
- updateDrawText = true;
- }
- if (isClose(size, expandedTitleTextSize)) {
- // If we're close to the expanded title size, snap to it and use a scale of 1
- titleScale = 1f;
- } else {
- // Else, we'll scale down from the expanded title size
- titleScale = size / expandedTitleTextSize;
- }
-
- final float textSizeRatio = collapsedTitleTextSize / expandedTitleTextSize;
- // This is the size of the expanded bounds when it is scaled to match the
- // collapsed title size
- final float scaledDownWidth = expandedWidth * textSizeRatio;
-
- if (scaledDownWidth > collapsedWidth) {
- // If the scaled down size is larger than the actual collapsed width, we need to
- // cap the available width so that when the expanded title scales down, it matches
- // the collapsed width
- availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth);
- } else {
- // Otherwise we'll just use the expanded width
- availableWidth = expandedWidth;
- }
- }
-
- if (availableWidth > 0) {
- updateDrawText = (currentTitleTextSize != newTextSize) || boundsChanged || updateDrawText;
- currentTitleTextSize = newTextSize;
- boundsChanged = false;
- }
-
- if (titleToDraw == null || updateDrawText) {
- titleTextPaint.setTextSize(currentTitleTextSize);
- titleTextPaint.setTypeface(currentTitleTypeface);
- // Use linear title scaling if we're scaling the canvas
- titleTextPaint.setLinearText(titleScale != 1f);
-
- // If we don't currently have title to draw, or the title size has changed, ellipsize...
- final CharSequence text =
- TextUtils
- .ellipsize(this.title, titleTextPaint, availableWidth, TextUtils.TruncateAt.END);
- if (!TextUtils.equals(text, titleToDraw)) {
- titleToDraw = text;
- isRtl = calculateIsRtl(titleToDraw);
- }
- }
- }
-
- @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
- private void calculateUsingSubtitleTextSize(final float size) {
- if (subtitle == null) {
- return;
- }
-
- final float collapsedWidth = collapsedBounds.width();
- final float expandedWidth = expandedBounds.width();
-
- final float availableWidth;
- final float newTextSize;
- boolean updateDrawText = false;
-
- if (isClose(size, collapsedSubtitleTextSize)) {
- newTextSize = collapsedSubtitleTextSize;
- subtitleScale = 1f;
- if (currentSubtitleTypeface != collapsedSubtitleTypeface) {
- currentSubtitleTypeface = collapsedSubtitleTypeface;
- updateDrawText = true;
- }
- availableWidth = collapsedWidth;
- } else {
- newTextSize = expandedSubtitleTextSize;
- if (currentSubtitleTypeface != expandedSubtitleTypeface) {
- currentSubtitleTypeface = expandedSubtitleTypeface;
- updateDrawText = true;
- }
- if (isClose(size, expandedSubtitleTextSize)) {
- // If we're close to the expanded title size, snap to it and use a scale of 1
- subtitleScale = 1f;
- } else {
- // Else, we'll scale down from the expanded title size
- subtitleScale = size / expandedSubtitleTextSize;
- }
-
- final float textSizeRatio = collapsedSubtitleTextSize / expandedSubtitleTextSize;
- // This is the size of the expanded bounds when it is scaled to match the
- // collapsed title size
- final float scaledDownWidth = expandedWidth * textSizeRatio;
-
- if (scaledDownWidth > collapsedWidth) {
- // If the scaled down size is larger than the actual collapsed width, we need to
- // cap the available width so that when the expanded title scales down, it matches
- // the collapsed width
- availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth);
- } else {
- // Otherwise we'll just use the expanded width
- availableWidth = expandedWidth;
- }
- }
-
- if (availableWidth > 0) {
- updateDrawText = (currentSubtitleTextSize != newTextSize) || boundsChanged || updateDrawText;
- currentSubtitleTextSize = newTextSize;
- boundsChanged = false;
- }
-
- if (subtitleToDraw == null || updateDrawText) {
- subtitleTextPaint.setTextSize(currentSubtitleTextSize);
- subtitleTextPaint.setTypeface(currentSubtitleTypeface);
- // Use linear title scaling if we're scaling the canvas
- subtitleTextPaint.setLinearText(subtitleScale != 1f);
-
- // If we don't currently have title to draw, or the title size has changed, ellipsize...
- final CharSequence text =
- TextUtils.ellipsize(this.subtitle, subtitleTextPaint, availableWidth, TextUtils.TruncateAt.END);
- if (!TextUtils.equals(text, subtitleToDraw)) {
- subtitleToDraw = text;
- isRtl = calculateIsRtl(subtitleToDraw);
- }
- }
- }
-
- private void ensureExpandedTitleTexture() {
- if (expandedTitleTexture != null || expandedBounds.isEmpty() || TextUtils.isEmpty(titleToDraw)) {
- return;
- }
-
- calculateOffsets(0f);
- titleTextureAscent = titleTextPaint.ascent();
- titleTextureDescent = titleTextPaint.descent();
-
- final int w = Math.round(titleTextPaint.measureText(titleToDraw, 0, titleToDraw.length()));
- final int h = Math.round(titleTextureDescent - titleTextureAscent);
-
- if (w <= 0 || h <= 0) {
- return; // If the width or height are 0, return
- }
-
- expandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
-
- Canvas c = new Canvas(expandedTitleTexture);
- c.drawText(titleToDraw, 0, titleToDraw.length(), 0, h - titleTextPaint.descent(), titleTextPaint);
-
- if (titleTexturePaint == null) {
- // Make sure we have a paint
- titleTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
- }
- }
-
- private void ensureExpandedSubtitleTexture() {
- if (expandedSubtitleTexture != null || expandedBounds.isEmpty() || TextUtils.isEmpty(subtitleToDraw)) {
- return;
- }
-
- calculateOffsets(0f);
- subtitleTextureAscent = subtitleTextPaint.ascent();
- subtitleTextureDescent = subtitleTextPaint.descent();
-
- final int w = Math.round(subtitleTextPaint.measureText(subtitleToDraw, 0, subtitleToDraw.length()));
- final int h = Math.round(subtitleTextureDescent - subtitleTextureAscent);
-
- if (w <= 0 || h <= 0) {
- return; // If the width or height are 0, return
- }
-
- expandedSubtitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
-
- Canvas c = new Canvas(expandedSubtitleTexture);
- c.drawText(subtitleToDraw, 0, subtitleToDraw.length(), 0, h - subtitleTextPaint.descent(), subtitleTextPaint);
-
- if (subtitleTexturePaint == null) {
- // Make sure we have a paint
- subtitleTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
- }
- }
-
- public void recalculate() {
- if (view.getHeight() > 0 && view.getWidth() > 0) {
- // If we've already been laid out, calculate everything now otherwise we'll wait
- // until a layout
- calculateBaseOffsets();
- calculateCurrentOffsets();
- }
- }
-
- /**
- * Set the title to display
- *
- * @param title
- */
- public void setTitle(@Nullable CharSequence title) {
- if (title == null || !title.equals(this.title)) {
- this.title = title;
- titleToDraw = null;
- clearTexture();
- recalculate();
- }
- }
-
- @Nullable
- public CharSequence getTitle() {
- return title;
- }
-
- /**
- * Set the subtitle to display
- *
- * @param subtitle
- */
- public void setSubtitle(@Nullable CharSequence subtitle) {
- if (subtitle == null || !subtitle.equals(this.subtitle)) {
- this.subtitle = subtitle;
- subtitleToDraw = null;
- clearTexture();
- recalculate();
- }
- }
-
- @Nullable
- public CharSequence getSubtitle() {
- return subtitle;
- }
-
- private void clearTexture() {
- if (expandedTitleTexture != null) {
- expandedTitleTexture.recycle();
- expandedTitleTexture = null;
- }
- if (expandedSubtitleTexture != null) {
- expandedSubtitleTexture.recycle();
- expandedSubtitleTexture = null;
- }
- }
-
- /**
- * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
- * defined as it's difference being < 0.001.
- */
- private static boolean isClose(float value, float targetValue) {
- return Math.abs(value - targetValue) < 0.001f;
- }
-
- public ColorStateList getExpandedTitleTextColor() {
- return expandedTitleTextColor;
- }
-
- public ColorStateList getExpandedSubtitleTextColor() {
- return expandedSubtitleTextColor;
- }
-
- public ColorStateList getCollapsedTitleTextColor() {
- return collapsedTitleTextColor;
- }
-
- public ColorStateList getCollapsedSubtitleTextColor() {
- return collapsedSubtitleTextColor;
- }
-
- /**
- * Blend {@code color1} and {@code color2} using the given ratio.
- *
- * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
- * 1.0 will return {@code color2}.
- */
- private static int blendColors(int color1, int color2, float ratio) {
- final float inverseRatio = 1f - ratio;
- float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
- float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio);
- float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio);
- float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio);
- return Color.argb((int) a, (int) r, (int) g, (int) b);
- }
-
- private static float lerp(
- float startValue, float endValue, float fraction, @Nullable TimeInterpolator interpolator) {
- if (interpolator != null) {
- fraction = interpolator.getInterpolation(fraction);
- }
- return AnimationUtils.lerp(startValue, endValue, fraction);
- }
-
- private static boolean rectEquals(@NonNull Rect r, int left, int top, int right, int bottom) {
- return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/App.java b/app/src/main/java/org/lsposed/manager/App.java
deleted file mode 100644
index 1e71a99fc..000000000
--- a/app/src/main/java/org/lsposed/manager/App.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager;
-
-import android.app.ActivityManager;
-import android.app.Application;
-import android.content.BroadcastReceiver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Process;
-import android.provider.MediaStore;
-import android.provider.Settings;
-import android.system.Os;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatDelegate;
-import androidx.preference.PreferenceManager;
-
-import org.lsposed.hiddenapibypass.HiddenApiBypass;
-import org.lsposed.manager.adapters.AppHelper;
-import org.lsposed.manager.receivers.LSPManagerServiceHolder;
-import org.lsposed.manager.repo.RepoLoader;
-import org.lsposed.manager.util.CloudflareDNS;
-import org.lsposed.manager.util.ModuleUtil;
-import org.lsposed.manager.util.ThemeUtil;
-import org.lsposed.manager.util.UpdateUtil;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.charset.StandardCharsets;
-import java.time.OffsetDateTime;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.FutureTask;
-
-import okhttp3.Cache;
-import okhttp3.OkHttpClient;
-import okhttp3.logging.HttpLoggingInterceptor;
-import rikka.core.os.FileUtils;
-import rikka.material.app.LocaleDelegate;
-
-public class App extends Application {
- public static final int PER_USER_RANGE = 100000;
- public static final FutureTask HTML_TEMPLATE = new FutureTask<>(() -> readWebviewHTML("template.html"));
- public static final FutureTask HTML_TEMPLATE_DARK = new FutureTask<>(() -> readWebviewHTML("template_dark.html"));
-
- private static String readWebviewHTML(String name) {
- try {
- var input = App.getInstance().getAssets().open("webview/" + name);
- var result = new ByteArrayOutputStream(1024);
- FileUtils.copy(input, result);
- return result.toString(StandardCharsets.UTF_8.name());
- } catch (IOException e) {
- Log.e(App.TAG, "read webview HTML", e);
- return "@body@";
- }
- }
-
- static {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- HiddenApiBypass.addHiddenApiExemptions("");
- }
- Looper.myQueue().addIdleHandler(() -> {
- if (App.getInstance() == null || App.getExecutorService() == null) return true;
- App.getExecutorService().submit(() -> {
- var list = AppHelper.getAppList(false);
- var pm = App.getInstance().getPackageManager();
- list.parallelStream().forEach(i -> AppHelper.getAppLabel(i, pm));
- ModuleUtil.getInstance();
- RepoLoader.getInstance();
- });
- App.getExecutorService().submit(HTML_TEMPLATE);
- App.getExecutorService().submit(HTML_TEMPLATE_DARK);
- return false;
- });
- }
-
- public static final String TAG = "LSPosedManager";
- private static final String ACTION_USER_ADDED = "android.intent.action.USER_ADDED";
- private static final String ACTION_USER_REMOVED = "android.intent.action.USER_REMOVED";
- private static final String ACTION_USER_INFO_CHANGED = "android.intent.action.USER_INFO_CHANGED";
- private static final String EXTRA_REMOVED_FOR_ALL_USERS = "android.intent.extra.REMOVED_FOR_ALL_USERS";
- private static App instance = null;
- private static OkHttpClient okHttpClient;
- private static Cache okHttpCache;
- private SharedPreferences pref;
- private static final ExecutorService executorService = Executors.newCachedThreadPool();
- private static final Handler MainHandler = new Handler(Looper.getMainLooper());
-
- public static App getInstance() {
- return instance;
- }
-
- public static SharedPreferences getPreferences() {
- return instance.pref;
- }
-
- public static ExecutorService getExecutorService() {
- return executorService;
- }
-
- public static final boolean isParasitic = !Process.isApplicationUid(Process.myUid());
-
- public static Handler getMainHandler() {
- return MainHandler;
- }
-
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
- var map = new HashMap(1);
- map.put("isParasitic", String.valueOf(isParasitic));
- var am = getSystemService(ActivityManager.class);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- map.clear();
- var reasons = am.getHistoricalProcessExitReasons(null, 0, 1);
- if (reasons.size() == 1) {
- map.put("description", reasons.get(0).getDescription());
- map.put("importance", String.valueOf(reasons.get(0).getImportance()));
- map.put("process", reasons.get(0).getProcessName());
- map.put("reason", String.valueOf(reasons.get(0).getReason()));
- map.put("status", String.valueOf(reasons.get(0).getStatus()));
- }
- }
- }
-
- private void setCrashReport() {
- var handler = Thread.getDefaultUncaughtExceptionHandler();
- Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
- var time = OffsetDateTime.now();
- var dir = new File(getCacheDir(), "crash");
- //noinspection ResultOfMethodCallIgnored
- dir.mkdir();
- var file = new File(dir, time.toEpochSecond() + ".log");
- try (var pw = new PrintWriter(file)) {
- pw.println(BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")");
- pw.println(time);
- pw.println("pid: " + Os.getpid() + " uid: " + Os.getuid());
- throwable.printStackTrace(pw);
- } catch (IOException ignored) {
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- var table = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
- var values = new ContentValues();
- values.put(MediaStore.Downloads.DISPLAY_NAME, "LSPosed_crash_report" + time.toEpochSecond() + ".zip");
- values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS);
- var cr = getContentResolver();
- var uri = cr.insert(table, values);
- if (uri == null) return;
- try (var zipFd = cr.openFileDescriptor(uri, "wt")) {
- LSPManagerServiceHolder.getService().getLogs(zipFd);
- } catch (Exception ignored) {
- cr.delete(uri, null, null);
- }
- }
- if (handler != null) {
- handler.uncaughtException(thread, throwable);
- }
- });
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- instance = this;
-
- setCrashReport();
- pref = PreferenceManager.getDefaultSharedPreferences(this);
- if (!pref.contains("doh")) {
- var name = "private_dns_mode";
- if ("hostname".equals(Settings.Global.getString(getContentResolver(), name))) {
- pref.edit().putBoolean("doh", false).apply();
- } else {
- pref.edit().putBoolean("doh", true).apply();
- }
- }
- AppCompatDelegate.setDefaultNightMode(ThemeUtil.getDarkTheme());
- LocaleDelegate.setDefaultLocale(getLocale());
- var res = getResources();
- var config = res.getConfiguration();
- config.setLocale(LocaleDelegate.getDefaultLocale());
- //noinspection deprecation
- res.updateConfiguration(config, res.getDisplayMetrics());
-
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction("org.lsposed.manager.NOTIFICATION");
- registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent inIntent) {
- var intent = (Intent) inIntent.getParcelableExtra(Intent.EXTRA_INTENT);
- Log.d(TAG, "onReceive: " + intent);
- switch (intent.getAction()) {
- case Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_FULLY_REMOVED, Intent.ACTION_UID_REMOVED -> {
- var userId = intent.getIntExtra(Intent.EXTRA_USER, 0);
- var packageName = intent.getStringExtra("android.intent.extra.PACKAGES");
- var packageRemovedForAllUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false);
- var isXposedModule = intent.getBooleanExtra("isXposedModule", false);
- if (packageName != null) {
- if (isXposedModule)
- ModuleUtil.getInstance().reloadSingleModule(packageName, userId, packageRemovedForAllUsers);
- else
- App.getExecutorService().submit(() -> AppHelper.getAppList(true));
- }
- }
- case ACTION_USER_ADDED, ACTION_USER_REMOVED, ACTION_USER_INFO_CHANGED -> App.getExecutorService().submit(() -> ModuleUtil.getInstance().reloadInstalledModules());
- }
- }
- }, intentFilter, Context.RECEIVER_NOT_EXPORTED);
-
- UpdateUtil.loadRemoteVersion();
- }
-
- @NonNull
- public static OkHttpClient getOkHttpClient() {
- if (okHttpClient != null) return okHttpClient;
- var builder = new OkHttpClient.Builder()
- .cache(getOkHttpCache())
- .dns(new CloudflareDNS());
- if (BuildConfig.DEBUG) {
- var log = new HttpLoggingInterceptor();
- log.setLevel(HttpLoggingInterceptor.Level.HEADERS);
- builder.addInterceptor(log);
- }
- okHttpClient = builder.build();
- return okHttpClient;
- }
-
- @NonNull
- public static Cache getOkHttpCache() {
- if (okHttpCache != null) return okHttpCache;
- long size50MiB = 50 * 1024 * 1024;
- okHttpCache = new Cache(new File(instance.getCacheDir(), "http_cache"), size50MiB);
- return okHttpCache;
- }
-
- public static Locale getLocale(String tag) {
- if (TextUtils.isEmpty(tag) || "SYSTEM".equals(tag)) {
- return LocaleDelegate.getSystemLocale();
- }
- return Locale.forLanguageTag(tag);
- }
-
- public static Locale getLocale() {
- String tag = getPreferences().getString("language", null);
- return getLocale(tag);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ConfigManager.java b/app/src/main/java/org/lsposed/manager/ConfigManager.java
deleted file mode 100644
index 3dbc9e8d2..000000000
--- a/app/src/main/java/org/lsposed/manager/ConfigManager.java
+++ /dev/null
@@ -1,347 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager;
-
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.ParcelFileDescriptor;
-import android.os.RemoteException;
-import android.util.Log;
-
-import org.lsposed.lspd.ILSPManagerService;
-import org.lsposed.lspd.models.Application;
-import org.lsposed.lspd.models.UserInfo;
-import org.lsposed.manager.adapters.ScopeAdapter;
-import org.lsposed.manager.receivers.LSPManagerServiceHolder;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
-
-public class ConfigManager {
-
- public static boolean isBinderAlive() {
- return LSPManagerServiceHolder.getService() != null;
- }
-
- public static int getXposedApiVersion() {
- try {
- return LSPManagerServiceHolder.getService().getXposedApiVersion();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return -1;
- }
- }
-
- public static String getXposedVersionName() {
- try {
- return LSPManagerServiceHolder.getService().getXposedVersionName();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return "";
- }
- }
-
- public static long getXposedVersionCode() {
- try {
- return LSPManagerServiceHolder.getService().getXposedVersionCode();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return -1;
- }
- }
-
- public static List getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) {
- List list = new ArrayList<>();
- try {
- list.addAll(LSPManagerServiceHolder.getService().getInstalledPackagesFromAllUsers(flags, filterNoProcess).getList());
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- }
- return list;
- }
-
- public static String[] getEnabledModules() {
- try {
- return LSPManagerServiceHolder.getService().enabledModules();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return new String[0];
- }
- }
-
- public static boolean setModuleEnabled(String packageName, boolean enable) {
- try {
- return enable ? LSPManagerServiceHolder.getService().enableModule(packageName) : LSPManagerServiceHolder.getService().disableModule(packageName);
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean setModuleScope(String packageName, boolean legacy, Set applications) {
- try {
- List list = new ArrayList<>();
- applications.forEach(application -> {
- Application app = new Application();
- app.userId = application.userId;
- app.packageName = application.packageName;
- list.add(app);
- });
- if (legacy) {
- Application app = new Application();
- app.userId = 0;
- app.packageName = packageName;
- list.add(app);
- }
- return LSPManagerServiceHolder.getService().setModuleScope(packageName, list);
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static List getModuleScope(String packageName) {
- List list = new ArrayList<>();
- try {
- var applications = LSPManagerServiceHolder.getService().getModuleScope(packageName);
- if (applications == null) {
- return list;
- }
- applications.forEach(application -> {
- if (!application.packageName.equals(packageName)) {
- list.add(new ScopeAdapter.ApplicationWithEquals(application));
- }
- });
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- }
- return list;
- }
-
- public static boolean enableStatusNotification() {
- try {
- return LSPManagerServiceHolder.getService().enableStatusNotification();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean setEnableStatusNotification(boolean enabled) {
- try {
- LSPManagerServiceHolder.getService().setEnableStatusNotification(enabled);
- return true;
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean isVerboseLogEnabled() {
- try {
- return LSPManagerServiceHolder.getService().isVerboseLog();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean setVerboseLogEnabled(boolean enabled) {
- try {
- LSPManagerServiceHolder.getService().setVerboseLog(enabled);
- return true;
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static ParcelFileDescriptor getLog(boolean verbose) {
- try {
- return verbose ? LSPManagerServiceHolder.getService().getVerboseLog() : LSPManagerServiceHolder.getService().getModulesLog();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return null;
- }
- }
-
- public static boolean clearLogs(boolean verbose) {
- try {
- return LSPManagerServiceHolder.getService().clearLogs(verbose);
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static PackageInfo getPackageInfo(String packageName, int flags, int userId) throws PackageManager.NameNotFoundException {
- try {
- var info = LSPManagerServiceHolder.getService().getPackageInfo(packageName, flags, userId);
- if (info == null) throw new PackageManager.NameNotFoundException();
- return info;
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- throw new PackageManager.NameNotFoundException();
- }
- }
-
- public static boolean forceStopPackage(String packageName, int userId) {
- try {
- LSPManagerServiceHolder.getService().forceStopPackage(packageName, userId);
- return true;
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean reboot() {
- try {
- LSPManagerServiceHolder.getService().reboot();
- return true;
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean uninstallPackage(String packageName, int userId) {
- try {
- return LSPManagerServiceHolder.getService().uninstallPackage(packageName, userId);
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean isSepolicyLoaded() {
- try {
- return LSPManagerServiceHolder.getService().isSepolicyLoaded();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static List getUsers() {
- try {
- return LSPManagerServiceHolder.getService().getUsers();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return null;
- }
- }
-
- public static boolean installExistingPackageAsUser(String packageName, int userId) {
- final int INSTALL_SUCCEEDED = 1;
- try {
- var ret = LSPManagerServiceHolder.getService().installExistingPackageAsUser(packageName, userId);
- return ret == INSTALL_SUCCEEDED;
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean isMagiskInstalled() {
- var path = System.getenv("PATH");
- if (path == null) return false;
- else return Arrays.stream(path.split(File.pathSeparator))
- .anyMatch(str -> new File(str, "magisk").exists());
- }
-
- public static boolean systemServerRequested() {
- try {
- return LSPManagerServiceHolder.getService().systemServerRequested();
- } catch (RemoteException e) {
- return false;
- }
- }
-
- public static boolean dex2oatFlagsLoaded() {
- try {
- return LSPManagerServiceHolder.getService().dex2oatFlagsLoaded();
- } catch (RemoteException e) {
- return false;
- }
- }
-
- public static int startActivityAsUserWithFeature(Intent intent, int userId) {
- try {
- return LSPManagerServiceHolder.getService().startActivityAsUserWithFeature(intent, userId);
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return -1;
- }
- }
-
- public static List queryIntentActivitiesAsUser(Intent intent, int flags, int userId) {
- List list = new ArrayList<>();
- try {
- list.addAll(LSPManagerServiceHolder.getService().queryIntentActivitiesAsUser(intent, flags, userId).getList());
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- }
- return list;
- }
-
- public static boolean setHiddenIcon(boolean hide) {
- try {
- LSPManagerServiceHolder.getService().setHiddenIcon(hide);
- return true;
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static int getDex2OatWrapperCompatibility() {
- try {
- return LSPManagerServiceHolder.getService().getDex2OatWrapperCompatibility();
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return ILSPManagerService.DEX2OAT_CRASHED;
- }
- }
-
- public static boolean getAutoInclude(String packageName) {
- try {
- return LSPManagerServiceHolder.getService().getAutoInclude(packageName);
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-
- public static boolean setAutoInclude(String packageName, boolean enable) {
- try {
- LSPManagerServiceHolder.getService().setAutoInclude(packageName, enable);
- return true;
- } catch (RemoteException e) {
- Log.e(App.TAG, Log.getStackTraceString(e));
- return false;
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/Constants.java b/app/src/main/java/org/lsposed/manager/Constants.java
deleted file mode 100644
index 7fd5817a3..000000000
--- a/app/src/main/java/org/lsposed/manager/Constants.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager;
-
-import android.os.IBinder;
-
-import org.lsposed.manager.receivers.LSPManagerServiceHolder;
-
-public class Constants {
- public static boolean setBinder(IBinder binder) {
- LSPManagerServiceHolder.init(binder);
- return LSPManagerServiceHolder.getService().asBinder().isBinderAlive();
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/adapters/AppHelper.java b/app/src/main/java/org/lsposed/manager/adapters/AppHelper.java
deleted file mode 100644
index f57d8d8bd..000000000
--- a/app/src/main/java/org/lsposed/manager/adapters/AppHelper.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.adapters;
-
-import android.annotation.SuppressLint;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Parcel;
-import android.view.MenuItem;
-
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
-
-public class AppHelper {
-
- public static final String SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS";
- public static final int FLAG_SHOW_FOR_ALL_USERS = 0x0400;
- private static List denyList;
- private static List appList;
- private static final ConcurrentHashMap appLabel = new ConcurrentHashMap<>();
-
- @SuppressLint("WrongConstant")
- public static Intent getSettingsIntent(String packageName, int userId) {
- Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
- intentToResolve.addCategory(SETTINGS_CATEGORY);
- intentToResolve.setPackage(packageName);
-
- List ris = ConfigManager.queryIntentActivitiesAsUser(intentToResolve, 0, userId);
-
- if (ris.size() == 0) {
- return getLaunchIntentForPackage(packageName, userId);
- }
-
- Intent intent = new Intent(intentToResolve);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setClassName(ris.get(0).activityInfo.packageName,
- ris.get(0).activityInfo.name);
- intent.putExtra("lsp_no_switch_to_user", (ris.get(0).activityInfo.flags & FLAG_SHOW_FOR_ALL_USERS) != 0);
- return intent;
- }
-
- @SuppressLint("WrongConstant")
- public static Intent getLaunchIntentForPackage(String packageName, int userId) {
- Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
- intentToResolve.addCategory(Intent.CATEGORY_INFO);
- intentToResolve.setPackage(packageName);
- List ris = ConfigManager.queryIntentActivitiesAsUser(intentToResolve, 0, userId);
-
- if (ris.size() == 0) {
- intentToResolve.removeCategory(Intent.CATEGORY_INFO);
- intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
- intentToResolve.setPackage(packageName);
- ris = ConfigManager.queryIntentActivitiesAsUser(intentToResolve, 0, userId);
- }
-
- if (ris.size() == 0) {
- return null;
- }
-
- Intent intent = new Intent(intentToResolve);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setClassName(ris.get(0).activityInfo.packageName,
- ris.get(0).activityInfo.name);
- intent.putExtra("lsp_no_switch_to_user", (ris.get(0).activityInfo.flags & FLAG_SHOW_FOR_ALL_USERS) != 0);
- return intent;
- }
-
- public static boolean onOptionsItemSelected(MenuItem item, SharedPreferences preferences) {
- int itemId = item.getItemId();
- int i = preferences.getInt("list_sort", 0);
- if (itemId == R.id.item_sort_by_name) {
- i = (i % 2 == 0) ? 0 : 1;
- } else if (itemId == R.id.item_sort_by_package_name) {
- i = (i % 2 == 0) ? 2 : 3;
- } else if (itemId == R.id.item_sort_by_install_time) {
- i = (i % 2 == 0) ? 4 : 5;
- } else if (itemId == R.id.item_sort_by_update_time) {
- i = (i % 2 == 0) ? 6 : 7;
- } else if (itemId == R.id.reverse) {
- if (i % 2 == 0) i++;
- else i--;
- } else {
- return false;
- }
- preferences.edit().putInt("list_sort", i).apply();
- if (item.isCheckable())
- item.setChecked(!item.isChecked());
- return true;
- }
-
- public static Comparator getAppListComparator(int sort, PackageManager pm) {
- ApplicationInfo.DisplayNameComparator displayNameComparator = new ApplicationInfo.DisplayNameComparator(pm);
- return switch (sort) {
- case 7 ->
- Collections.reverseOrder(Comparator.comparingLong((PackageInfo a) -> a.lastUpdateTime));
- case 6 -> Comparator.comparingLong((PackageInfo a) -> a.lastUpdateTime);
- case 5 ->
- Collections.reverseOrder(Comparator.comparingLong((PackageInfo a) -> a.firstInstallTime));
- case 4 -> Comparator.comparingLong((PackageInfo a) -> a.firstInstallTime);
- case 3 -> Collections.reverseOrder(Comparator.comparing(a -> a.packageName));
- case 2 -> Comparator.comparing(a -> a.packageName);
- case 1 ->
- Collections.reverseOrder((PackageInfo a, PackageInfo b) -> displayNameComparator.compare(a.applicationInfo, b.applicationInfo));
- default ->
- (PackageInfo a, PackageInfo b) -> displayNameComparator.compare(a.applicationInfo, b.applicationInfo);
- };
- }
-
- synchronized public static List getAppList(boolean force) {
- if (appList == null || force) {
- appList = ConfigManager.getInstalledPackagesFromAllUsers(PackageManager.GET_META_DATA | PackageManager.MATCH_UNINSTALLED_PACKAGES, true);
- PackageInfo system = null;
- for (var app : appList) {
- if ("android".equals(app.packageName)) {
- var p = Parcel.obtain();
- app.writeToParcel(p, 0);
- p.setDataPosition(0);
- system = PackageInfo.CREATOR.createFromParcel(p);
- system.packageName = "system";
- system.applicationInfo.packageName = system.packageName;
- break;
- }
- }
- if (system != null) {
- appList.add(system);
- }
- }
- return appList;
- }
-
- public static CharSequence getAppLabel(PackageInfo info, PackageManager pm) {
- if (info == null || info.applicationInfo == null) return null;
- return appLabel.computeIfAbsent(info, i -> i.applicationInfo.loadLabel(pm));
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/adapters/ScopeAdapter.java b/app/src/main/java/org/lsposed/manager/adapters/ScopeAdapter.java
deleted file mode 100644
index 454c5d139..000000000
--- a/app/src/main/java/org/lsposed/manager/adapters/ScopeAdapter.java
+++ /dev/null
@@ -1,733 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.adapters;
-
-import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Build;
-import android.text.Spannable;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
-import android.text.style.TypefaceSpan;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CompoundButton;
-import android.widget.Filter;
-import android.widget.Filterable;
-import android.widget.ImageView;
-import android.widget.Switch;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.SearchView;
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.bumptech.glide.request.target.CustomTarget;
-import com.bumptech.glide.request.transition.Transition;
-import com.google.android.material.checkbox.MaterialCheckBox;
-
-import org.lsposed.lspd.models.Application;
-import org.lsposed.manager.App;
-import org.lsposed.manager.BuildConfig;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.ItemMasterSwitchBinding;
-import org.lsposed.manager.databinding.ItemModuleBinding;
-import org.lsposed.manager.databinding.ItemScopeFooterBinding;
-import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
-import org.lsposed.manager.ui.fragment.AppListFragment;
-import org.lsposed.manager.ui.fragment.CompileDialogFragment;
-import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
-import org.lsposed.manager.util.GlideApp;
-import org.lsposed.manager.util.ModuleUtil;
-
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import rikka.core.util.ResourceUtils;
-import rikka.material.app.LocaleDelegate;
-import rikka.widget.mainswitchbar.MainSwitchBar;
-import rikka.widget.mainswitchbar.OnMainSwitchChangeListener;
-
-public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter implements Filterable {
-
- private final Activity activity;
- private final AppListFragment fragment;
- private final PackageManager pm;
- private final SharedPreferences preferences;
- private final ModuleUtil moduleUtil;
-
- private final ModuleUtil.InstalledModule module;
-
- private Set recommendedList = new HashSet<>();
- private Set checkedList = new HashSet<>();
- private List searchList = new ArrayList<>();
- private List showList = new ArrayList<>();
-
- public RecyclerView.Adapter switchAdaptor = new RecyclerView.Adapter<>() {
- @NonNull
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new RecyclerView.ViewHolder(ItemMasterSwitchBinding.inflate(activity.getLayoutInflater(), parent, false).masterSwitch) {
- };
- }
-
- @Override
- public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
- var mainSwitchBar = (MainSwitchBar) holder.itemView;
- mainSwitchBar.setChecked(enabled);
- mainSwitchBar.addOnSwitchChangeListener(switchBarOnCheckedChangeListener);
- }
-
- @Override
- public int getItemCount() {
- return 1;
- }
- };
-
- /**
- * staticScope says the module's scope list is the whole of it, so the list above shows only the
- * apps it claims and this rules it off. Nothing follows for a module that does not claim one.
- */
- public RecyclerView.Adapter footerAdaptor = new RecyclerView.Adapter<>() {
- @NonNull
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new RecyclerView.ViewHolder(ItemScopeFooterBinding.inflate(activity.getLayoutInflater(), parent, false).getRoot()) {
- };
- }
-
- @Override
- public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
- }
-
- @Override
- public int getItemCount() {
- return module.staticScope ? 1 : 0;
- }
- };
-
- private final OnMainSwitchChangeListener switchBarOnCheckedChangeListener = new OnMainSwitchChangeListener() {
- @Override
- public void onSwitchChanged(Switch view, boolean isChecked) {
- enabled = isChecked;
- if (!moduleUtil.setModuleEnabled(module.packageName, isChecked)) {
- view.setChecked(!isChecked);
- enabled = !isChecked;
- }
- var tmpChkList = new HashSet<>(checkedList);
- if (isChecked && !tmpChkList.isEmpty() && !ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList)) {
- view.setChecked(false);
- enabled = false;
- }
- fragment.runOnUiThread(ScopeAdapter.this::notifyDataSetChanged);
- }
- };
-
- private ApplicationInfo selectedApplicationInfo;
- private boolean isLoaded = false;
- private boolean enabled = true;
-
- public ScopeAdapter(AppListFragment fragment, ModuleUtil.InstalledModule module) {
- this.fragment = fragment;
- this.activity = fragment.requireActivity();
- this.module = module;
- moduleUtil = ModuleUtil.getInstance();
- preferences = App.getPreferences();
- pm = activity.getPackageManager();
- }
-
- @NonNull
- @Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new ViewHolder(ItemModuleBinding.inflate(activity.getLayoutInflater(), parent, false));
- }
-
- private boolean shouldHideApp(PackageInfo info, ApplicationWithEquals app, HashSet tmpChkList) {
- if (info.packageName.equals("system")) {
- return false;
- }
- if (tmpChkList.contains(app)) {
- return false;
- }
- if (preferences.getBoolean("filter_modules", true)) {
- if (ModuleUtil.getInstance().getModule(info.packageName, info.applicationInfo.uid / App.PER_USER_RANGE) != null) {
- return true;
- }
- }
- if (preferences.getBoolean("filter_games", true)) {
- if (info.applicationInfo.category == ApplicationInfo.CATEGORY_GAME) {
- return true;
- }
- //noinspection deprecation
- if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_GAME) != 0) {
- return true;
- }
- }
- return preferences.getBoolean("filter_system_apps", true) && (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
- }
-
- private int sortApps(AppInfo x, AppInfo y) {
- Comparator comparator = AppHelper.getAppListComparator(preferences.getInt("list_sort", 0), pm);
- Comparator frameworkComparator = (a, b) -> {
- if (a.packageName.equals("system") == b.packageName.equals("system")) {
- return comparator.compare(a.packageInfo, b.packageInfo);
- } else if (a.packageName.equals("system")) {
- return -1;
- } else {
- return 1;
- }
- };
- Comparator recommendedComparator = (a, b) -> {
- boolean aRecommended = !recommendedList.isEmpty() && recommendedList.contains(a.application);
- boolean bRecommended = !recommendedList.isEmpty() && recommendedList.contains(b.application);
- if (aRecommended == bRecommended) {
- return frameworkComparator.compare(a, b);
- } else if (aRecommended) {
- return -1;
- } else {
- return 1;
- }
- };
- boolean aChecked = checkedList.contains(x.application);
- boolean bChecked = checkedList.contains(y.application);
- if (aChecked == bChecked) {
- return recommendedComparator.compare(x, y);
- } else if (aChecked) {
- return -1;
- } else {
- return 1;
- }
- }
-
- private void checkRecommended() {
- if (!enabled) {
- fragment.showHint(R.string.module_is_not_activated_yet, false);
- return;
- }
- fragment.runAsync(() -> {
- var tmpChkList = new HashSet<>(checkedList);
- tmpChkList.removeIf(i -> i.userId == module.userId);
- tmpChkList.addAll(recommendedList);
- ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList);
- checkedList = tmpChkList;
- fragment.runOnUiThread(this::notifyDataSetChanged);
- });
- }
-
- @SuppressLint("NotifyDataSetChanged")
- private void setLoaded(List list, boolean loaded) {
- fragment.runOnUiThread(() -> {
- if (list != null) showList = list;
- isLoaded = loaded;
- notifyDataSetChanged();
- });
- }
-
- public boolean onOptionsItemSelected(MenuItem item) {
- int itemId = item.getItemId();
- if (itemId == R.id.use_recommended) {
- if (!checkedList.isEmpty()) {
- new BlurBehindDialogBuilder(activity, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons)
- .setMessage(R.string.use_recommended_message)
- .setPositiveButton(android.R.string.ok, (dialog, which) -> checkRecommended())
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- } else {
- checkRecommended();
- }
- return true;
- } else if (itemId == R.id.item_filter_system) {
- item.setChecked(!item.isChecked());
- preferences.edit().putBoolean("filter_system_apps", item.isChecked()).apply();
- } else if (itemId == R.id.item_filter_games) {
- item.setChecked(!item.isChecked());
- preferences.edit().putBoolean("filter_games", item.isChecked()).apply();
- } else if (itemId == R.id.item_filter_modules) {
- item.setChecked(!item.isChecked());
- preferences.edit().putBoolean("filter_modules", item.isChecked()).apply();
- } else if (itemId == R.id.backup) {
- LocalDateTime now = LocalDateTime.now();
- try {
- fragment.backupLauncher.launch(String.format(LocaleDelegate.getDefaultLocale(),
- "%s_%s.lsp", module.getAppName(), now.toString()));
- return true;
- } catch (ActivityNotFoundException e) {
- fragment.showHint(R.string.enable_documentui, true);
- return false;
- }
- } else if (itemId == R.id.restore) {
- try {
- fragment.restoreLauncher.launch(new String[]{"*/*"});
- return true;
- } catch (ActivityNotFoundException e) {
- fragment.showHint(R.string.enable_documentui, true);
- return false;
- }
- } else if (itemId == R.id.select_all) {
- var tmpChkList = new HashSet(ConfigManager.getModuleScope(module.packageName));
- for (AppInfo info : searchList) {
- if (info.packageName.equals("android")) {
- fragment.showHint(R.string.reboot_required, true, R.string.reboot, v -> ConfigManager.reboot());
- }
- tmpChkList.add(info.application);
- }
- ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList);
- } else if (itemId == R.id.select_none) {
- var tmpChkList = new HashSet(ConfigManager.getModuleScope(module.packageName));
- for (AppInfo info : searchList) {
- if (tmpChkList.remove(info.application) && info.packageName.equals("android")) {
- fragment.showHint(R.string.reboot_required, true, R.string.reboot, v -> ConfigManager.reboot());
- }
- }
- ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList);
- } else if (itemId == R.id.auto_include) {
- item.setChecked(!item.isChecked());
- ConfigManager.setAutoInclude(module.packageName, item.isChecked());
- } else if (!AppHelper.onOptionsItemSelected(item, preferences)) {
- return false;
- }
- refresh();
- return true;
- }
-
- public boolean onContextItemSelected(@NonNull MenuItem item) {
- var info = selectedApplicationInfo;
- if (info == null) {
- return false;
- }
- int itemId = item.getItemId();
- if (itemId == R.id.menu_launch) {
- Intent launchIntent = AppHelper.getLaunchIntentForPackage(info.packageName, info.uid / App.PER_USER_RANGE);
- if (launchIntent != null) {
- ConfigManager.startActivityAsUserWithFeature(launchIntent, module.userId);
- }
- } else if (itemId == R.id.menu_compile_speed) {
- CompileDialogFragment.speed(fragment.getChildFragmentManager(), info);
- } else if (itemId == R.id.menu_other_app) {
- var intent = new Intent(Intent.ACTION_SHOW_APP_INFO);
- intent.putExtra(Intent.EXTRA_PACKAGE_NAME, module.packageName);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- ConfigManager.startActivityAsUserWithFeature(intent, module.userId);
- } else if (itemId == R.id.menu_app_info) {
- ConfigManager.startActivityAsUserWithFeature(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", info.packageName, null)), module.userId);
- } else if (itemId == R.id.menu_force_stop) {
- if (info.packageName.equals("system")) {
- new BlurBehindDialogBuilder(activity, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons)
- .setTitle(R.string.reboot)
- .setPositiveButton(android.R.string.ok, (dialog, which) -> ConfigManager.reboot())
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- } else {
- new BlurBehindDialogBuilder(activity, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons)
- .setTitle(R.string.force_stop_dlg_title)
- .setMessage(R.string.force_stop_dlg_text)
- .setPositiveButton(android.R.string.ok, (dialog, which) -> ConfigManager.forceStopPackage(info.packageName, info.uid / 100000))
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
- } else {
- return false;
- }
- return true;
- }
-
- public void onPrepareOptionsMenu(@NonNull Menu menu) {
- List scopeList = module.getScopeList();
- if (scopeList == null || scopeList.isEmpty()) {
- menu.removeItem(R.id.use_recommended);
- }
- menu.findItem(R.id.item_filter_system).setChecked(preferences.getBoolean("filter_system_apps", true));
- menu.findItem(R.id.item_filter_games).setChecked(preferences.getBoolean("filter_games", true));
- menu.findItem(R.id.item_filter_modules).setChecked(preferences.getBoolean("filter_modules", true));
- switch (preferences.getInt("list_sort", 0)) {
- case 7 -> {
- menu.findItem(R.id.item_sort_by_update_time).setChecked(true);
- menu.findItem(R.id.reverse).setChecked(true);
- }
- case 6 -> menu.findItem(R.id.item_sort_by_update_time).setChecked(true);
- case 5 -> {
- menu.findItem(R.id.item_sort_by_install_time).setChecked(true);
- menu.findItem(R.id.reverse).setChecked(true);
- }
- case 4 -> menu.findItem(R.id.item_sort_by_install_time).setChecked(true);
- case 3 -> {
- menu.findItem(R.id.item_sort_by_package_name).setChecked(true);
- menu.findItem(R.id.reverse).setChecked(true);
- }
- case 2 -> menu.findItem(R.id.item_sort_by_package_name).setChecked(true);
- case 1 -> {
- menu.findItem(R.id.item_sort_by_name).setChecked(true);
- menu.findItem(R.id.reverse).setChecked(true);
- }
- case 0 -> menu.findItem(R.id.item_sort_by_name).setChecked(true);
- }
- menu.findItem(R.id.auto_include).setChecked(ConfigManager.getAutoInclude(module.packageName));
- }
-
- @Override
- public void onViewRecycled(@NonNull ViewHolder holder) {
- if (holder.checkbox != null) {
- holder.checkbox.setOnCheckedChangeListener(null);
- }
- super.onViewRecycled(holder);
- }
-
- @Override
- public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
- AppInfo appInfo = showList.get(position);
- holder.root.setAlpha(enabled ? 1.0f : .5f);
- boolean system = appInfo.packageName.equals("system");
- CharSequence appName;
- int userId = appInfo.applicationInfo.uid / App.PER_USER_RANGE;
- appName = system ? activity.getString(R.string.android_framework) : appInfo.label;
- holder.appName.setText(appName);
- GlideApp.with(holder.appIcon).load(appInfo.packageInfo).into(new CustomTarget() {
- @Override
- public void onResourceReady(@NonNull Drawable resource, @Nullable Transition super Drawable> transition) {
- holder.appIcon.setImageDrawable(resource);
- }
-
- @Override
- public void onLoadCleared(@Nullable Drawable placeholder) {
-
- }
-
- @Override
- public void onLoadFailed(@Nullable Drawable errorDrawable) {
- holder.appIcon.setImageDrawable(pm.getDefaultActivityIcon());
- }
- });
- if (system) {
- //noinspection SetTextI18n
- holder.appPackageName.setText("system");
- holder.appVersionName.setVisibility(View.GONE);
- } else {
- holder.appVersionName.setVisibility(View.VISIBLE);
- holder.appPackageName.setText(appInfo.packageName);
- }
- holder.appPackageName.setVisibility(View.VISIBLE);
- holder.appVersionName.setText(activity.getString(R.string.app_version, appInfo.packageInfo.versionName));
- var sb = new SpannableStringBuilder();
- if (!recommendedList.isEmpty() && recommendedList.contains(appInfo.application)) {
- String recommended = activity.getString(R.string.requested_by_module);
- sb.append(recommended);
- final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(activity.getTheme(), com.google.android.material.R.attr.colorPrimary));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
- sb.setSpan(typefaceSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- } else {
- final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
- sb.setSpan(styleSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
- sb.setSpan(foregroundColorSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
- if (sb.length() == 0) {
- holder.hint.setVisibility(View.GONE);
- } else {
- holder.hint.setText(sb);
- holder.hint.setVisibility(View.VISIBLE);
- }
-
- holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
- activity.getMenuInflater().inflate(R.menu.menu_app_item, menu);
- menu.setHeaderTitle(appName);
- Intent launchIntent = AppHelper.getLaunchIntentForPackage(appInfo.packageName, userId);
- if (launchIntent == null) {
- menu.removeItem(R.id.menu_launch);
- }
- if (system) {
- menu.findItem(R.id.menu_force_stop).setTitle(R.string.reboot);
- menu.removeItem(R.id.menu_compile_speed);
- menu.removeItem(R.id.menu_other_app);
- menu.removeItem(R.id.menu_app_info);
- }
- });
-
- holder.checkbox.setChecked(checkedList.contains(appInfo.application));
-
- holder.checkbox.setOnCheckedChangeListener((v, isChecked) -> onCheckedChange(v, isChecked, appInfo));
-
- holder.itemView.setOnClickListener(v -> {
- if (enabled) holder.checkbox.toggle();
- });
- holder.itemView.setOnLongClickListener(v -> {
- fragment.searchView.clearFocus();
- selectedApplicationInfo = appInfo.applicationInfo;
- return false;
- });
- }
-
- @Override
- public long getItemId(int position) {
- PackageInfo info = showList.get(position).packageInfo;
- return (info.packageName + "!" + info.applicationInfo.uid / App.PER_USER_RANGE).hashCode();
- }
-
- @Override
- public Filter getFilter() {
- return new ApplicationFilter();
- }
-
- @Override
- public int getItemCount() {
- return showList.size();
- }
-
- public void refresh() {
- refresh(false);
- }
-
- public void refresh(boolean force) {
- setLoaded(null, false);
- enabled = moduleUtil.isModuleEnabled(module.packageName);
- fragment.runAsync(() -> {
- List appList = AppHelper.getAppList(force);
- var tmpRecList = new HashSet();
- var tmpChkList = new HashSet<>(ConfigManager.getModuleScope(module.packageName));
- final var tmpList = new ArrayList();
- final HashSet installedList = new HashSet<>();
- List scopeList = module.getScopeList();
- boolean emptyCheckedList = tmpChkList.isEmpty();
- appList.parallelStream().forEach(info -> {
- int userId = info.applicationInfo.uid / App.PER_USER_RANGE;
- String packageName = info.packageName;
- if (packageName.equals("system") && userId != 0 ||
- packageName.equals(module.packageName) ||
- packageName.equals(BuildConfig.APPLICATION_ID)) {
- return;
- }
-
- ApplicationWithEquals application = new ApplicationWithEquals(packageName, userId);
-
- synchronized (installedList) {
- installedList.add(application);
- }
-
- if (userId != module.userId) {
- return;
- }
-
- if (scopeList != null && scopeList.contains(packageName)) {
- synchronized (tmpRecList) {
- tmpRecList.add(application);
- }
- } else if (module.staticScope && !tmpChkList.contains(application)) {
- // The module says its scope list is the whole of it, so nothing outside that
- // list is offered. An app already enabled outside it still has to be shown, or
- // it would keep the module loaded from a row nobody can see to switch off.
- return;
- } else if (shouldHideApp(info, application, tmpChkList)) {
- return;
- }
-
- AppInfo appInfo = new AppInfo();
- appInfo.packageInfo = info;
- appInfo.label = AppHelper.getAppLabel(info, pm);
- appInfo.application = application;
- appInfo.packageName = info.packageName;
- appInfo.applicationInfo = info.applicationInfo;
- synchronized (tmpList) {
- tmpList.add(appInfo);
- }
- });
- tmpChkList.retainAll(installedList);
- checkedList = tmpChkList;
- recommendedList = tmpRecList;
- searchList = tmpList.parallelStream().sorted(this::sortApps).collect(Collectors.toList());
-
- String queryStr = fragment.searchView != null ? fragment.searchView.getQuery().toString() : "";
-
- fragment.runOnUiThread(() -> getFilter().filter(queryStr));
- });
- }
-
- protected void onCheckedChange(CompoundButton buttonView, boolean isChecked, AppInfo appInfo) {
- var tmpChkList = new HashSet<>(checkedList);
- if (isChecked) {
- tmpChkList.add(appInfo.application);
- } else {
- tmpChkList.remove(appInfo.application);
- }
- if (!ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList)) {
- fragment.showHint(R.string.failed_to_save_scope_list, true);
- if (!isChecked) {
- tmpChkList.add(appInfo.application);
- } else {
- tmpChkList.remove(appInfo.application);
- }
- buttonView.setChecked(!isChecked);
- } else if (appInfo.packageName.equals("system")) {
- fragment.showHint(R.string.reboot_required, true, R.string.reboot, v -> ConfigManager.reboot());
- }
- checkedList = tmpChkList;
- }
-
- @Override
- public boolean isLoaded() {
- return isLoaded;
- }
-
- public static class ViewHolder extends RecyclerView.ViewHolder {
- ConstraintLayout root;
- ImageView appIcon;
- TextView appName;
- TextView appPackageName;
- TextView appVersionName;
- TextView hint;
- MaterialCheckBox checkbox;
-
- ViewHolder(ItemModuleBinding binding) {
- super(binding.getRoot());
- root = binding.itemRoot;
- appIcon = binding.appIcon;
- appName = binding.appName;
- appPackageName = binding.appPackageName;
- appVersionName = binding.appVersionName;
- checkbox = binding.checkbox;
- hint = binding.hint;
- checkbox.setVisibility(View.VISIBLE);
- }
- }
-
- private class ApplicationFilter extends Filter {
-
- private boolean lowercaseContains(String s, String filter) {
- return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
- }
-
- @Override
- protected FilterResults performFiltering(CharSequence constraint) {
- FilterResults filterResults = new FilterResults();
- List filtered = new ArrayList<>();
- String filter = constraint.toString().toLowerCase();
- for (AppInfo info : searchList) {
- if (lowercaseContains(info.label.toString(), filter)
- || lowercaseContains(info.packageName, filter)) {
- filtered.add(info);
- }
- }
- filterResults.values = filtered;
- filterResults.count = filtered.size();
- return filterResults;
- }
-
- @Override
- protected void publishResults(CharSequence constraint, FilterResults results) {
- //noinspection unchecked
- setLoaded((List) results.values, true);
- }
- }
-
- public SearchView.OnQueryTextListener getSearchListener() {
- return new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- getFilter().filter(query);
- return true;
- }
-
- @Override
- public boolean onQueryTextChange(String query) {
- getFilter().filter(query);
- return true;
- }
- };
- }
-
- public void onBackPressed() {
- fragment.searchView.clearFocus();
- if (isLoaded && enabled && checkedList.isEmpty()) {
- var builder = new BlurBehindDialogBuilder(activity, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons);
- builder.setMessage(!recommendedList.isEmpty() ? R.string.no_scope_selected_has_recommended : R.string.no_scope_selected);
- if (!recommendedList.isEmpty()) {
- builder.setPositiveButton(android.R.string.ok, (dialog, which) -> checkRecommended());
- } else {
- builder.setPositiveButton(android.R.string.cancel, null);
- }
- builder.setNegativeButton(!recommendedList.isEmpty() ? android.R.string.cancel : android.R.string.ok, (dialog, which) -> {
- moduleUtil.setModuleEnabled(module.packageName, false);
- Toast.makeText(activity, activity.getString(R.string.module_disabled_no_selection, module.getAppName()), Toast.LENGTH_LONG).show();
- fragment.navigateUp();
- });
- builder.show();
- } else {
- fragment.navigateUp();
- }
- }
-
- public static class AppInfo {
- public PackageInfo packageInfo;
- public ApplicationWithEquals application;
- public ApplicationInfo applicationInfo;
- public String packageName;
- public CharSequence label = null;
- }
-
- public static class ApplicationWithEquals extends Application {
- public ApplicationWithEquals(String packageName, int userId) {
- this.packageName = packageName;
- this.userId = userId;
- }
-
- public ApplicationWithEquals(Application application) {
- packageName = application.packageName;
- userId = application.userId;
- }
-
- @Override
- public boolean equals(@Nullable Object obj) {
- if (!(obj instanceof Application)) {
- return false;
- }
- return packageName.equals(((Application) obj).packageName) && userId == ((Application) obj).userId;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(packageName, userId);
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/receivers/LSPManagerServiceHolder.java b/app/src/main/java/org/lsposed/manager/receivers/LSPManagerServiceHolder.java
deleted file mode 100644
index 64d36a0d5..000000000
--- a/app/src/main/java/org/lsposed/manager/receivers/LSPManagerServiceHolder.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.receivers;
-
-import android.os.IBinder;
-import android.os.Process;
-import android.os.RemoteException;
-import android.system.Os;
-
-import org.lsposed.lspd.ILSPManagerService;
-
-public class LSPManagerServiceHolder implements IBinder.DeathRecipient {
- private static LSPManagerServiceHolder holder = null;
- private static ILSPManagerService service = null;
-
- public static void init(IBinder binder) {
- if (holder == null) {
- holder = new LSPManagerServiceHolder(binder);
- }
- }
-
- public static ILSPManagerService getService() {
- return service;
- }
-
- private LSPManagerServiceHolder(IBinder binder) {
- linkToDeath(binder);
- service = ILSPManagerService.Stub.asInterface(binder);
- }
-
- private void linkToDeath(IBinder binder) {
- try {
- binder.linkToDeath(this, 0);
- } catch (RemoteException e) {
- binderDied();
- }
- }
-
- @Override
- public void binderDied() {
- System.exit(0);
- Process.killProcess(Os.getpid());
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java b/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java
deleted file mode 100644
index cd4ccffe2..000000000
--- a/app/src/main/java/org/lsposed/manager/repo/RepoLoader.java
+++ /dev/null
@@ -1,461 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.repo;
-
-import android.content.res.Resources;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.google.gson.Gson;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.R;
-import org.lsposed.manager.repo.model.OnlineModule;
-import org.lsposed.manager.repo.model.Release;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import okhttp3.Call;
-import okhttp3.Callback;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.ResponseBody;
-
-public class RepoLoader {
- private static RepoLoader instance = null;
- private Map onlineModules = new HashMap<>();
- private Map latestVersion = new ConcurrentHashMap<>();
-
- public static class ModuleVersion {
- public String versionName;
- public long versionCode;
-
- private ModuleVersion(long versionCode, String versionName) {
- this.versionName = versionName;
- this.versionCode = versionCode;
- }
-
- public boolean upgradable(long versionCode, String versionName) {
- return this.versionCode > versionCode || (this.versionCode == versionCode && !versionName.replace(' ', '_').equals(this.versionName));
- }
-
- }
-
- private final Path repoFile = Paths.get(App.getInstance().getFilesDir().getAbsolutePath(), "repo.json");
- private final Set listeners = ConcurrentHashMap.newKeySet();
- private boolean repoLoaded = false;
- // The full module list is only served by the backup host; modules.lsposed.org
- // returns 403 for modules.json, and the blogcdn/cloudflare mirrors are dead.
- private static final String[] listRepoUrls = new String[]{
- "https://backup.modules.lsposed.org/"
- };
- // Per-module detail JSON is served by both the backup host and the public
- // site, so the latter acts as a real fallback for module/.json.
- private static final String[] detailRepoUrls = new String[]{
- "https://backup.modules.lsposed.org/",
- "https://modules.lsposed.org/"
- };
- // Each module is mirrored as a GitHub repo under this org; used as a
- // last-resort README source when the JSON API omits it or is unreachable.
- private static final String moduleGithubReadmeUrl = "https://api.github.com/repos/Xposed-Modules-Repo/%s/readme";
- private final Resources resources = App.getInstance().getResources();
- private final String[] channels = resources.getStringArray(R.array.update_channel_values);
-
- public boolean isRepoLoaded() {
- return repoLoaded;
- }
-
- public static synchronized RepoLoader getInstance() {
- if (instance == null) {
- instance = new RepoLoader();
- App.getExecutorService().submit(() -> instance.loadLocalData(true));
- }
- return instance;
- }
-
- synchronized public void loadRemoteData() {
- repoLoaded = false;
- boolean loaded = false;
- Throwable lastError = null;
- try {
- for (String candidateRepoUrl : listRepoUrls) {
- try {
- String bodyString = requestString(candidateRepoUrl + "modules.json");
- OnlineModule[] repoModules = parseRepoModules(bodyString);
- Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8));
- Log.i(App.TAG, "repo: fetched module list from " + candidateRepoUrl + " (" + repoModules.length + " entries, " + bodyString.length() + " bytes)");
- replaceRepoModules(repoModules);
- loaded = true;
- break;
- } catch (Throwable t) {
- lastError = t;
- Log.e(App.TAG, "load remote data from " + candidateRepoUrl, t);
- }
- }
- if (!loaded && lastError != null) {
- for (RepoListener listener : listeners) {
- listener.onThrowable(lastError);
- }
- }
- } finally {
- if (!loaded) {
- Log.w(App.TAG, "repo: module list load failed on all mirrors, keeping cached data (" + onlineModules.size() + " modules)");
- repoLoaded = true;
- for (RepoListener listener : listeners) {
- listener.onRepoLoaded();
- }
- }
- }
- }
-
- private OnlineModule[] parseRepoModules(String bodyString) throws IOException {
- Gson gson = new Gson();
- OnlineModule[] repoModules = gson.fromJson(bodyString, OnlineModule[].class);
- if (repoModules == null) {
- throw new IOException("Invalid repo response");
- }
- return repoModules;
- }
-
- private void replaceRepoModules(OnlineModule[] repoModules) {
- Map modules = new HashMap<>();
- Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule));
- var channel = App.getPreferences().getString("update_channel", channels[0]);
- onlineModules = modules;
- Log.i(App.TAG, "repo: onlineModules replaced, now " + modules.size() + " modules (channel=" + channel + ")");
- updateLatestVersion(repoModules, channel);
- }
-
- private String requestString(String url) throws IOException {
- try (var response = App.getOkHttpClient().newCall(new Request.Builder().url(url).build()).execute()) {
- if (!response.isSuccessful()) {
- throw new IOException("Unexpected response " + response.code() + " from " + response.request().url());
- }
- ResponseBody body = response.body();
- if (body == null) {
- throw new IOException("Empty response from " + response.request().url());
- }
- String bodyString = body.string();
- if (bodyString.trim().isEmpty()) {
- throw new IOException("Empty response from " + response.request().url());
- }
- return bodyString;
- }
- }
-
- synchronized public void loadLocalData(boolean updateRemoteRepo) {
- repoLoaded = false;
- Log.i(App.TAG, "repo: loadLocalData(updateRemoteRepo=" + updateRemoteRepo + "), cacheExists=" + Files.exists(repoFile));
- try {
- if (Files.notExists(repoFile)) {
- loadRemoteData();
- updateRemoteRepo = false;
- }
- byte[] encoded = Files.readAllBytes(repoFile);
- String bodyString = new String(encoded, StandardCharsets.UTF_8);
- OnlineModule[] repoModules = parseRepoModules(bodyString);
- Log.i(App.TAG, "repo: loadLocalData parsed " + repoModules.length + " modules from cache (" + encoded.length + " bytes)");
- replaceRepoModules(repoModules);
- } catch (Throwable t) {
- Log.e(App.TAG, "repo: loadLocalData failed", t);
- for (RepoListener listener : listeners) {
- listener.onThrowable(t);
- }
- } finally {
- repoLoaded = true;
- for (RepoListener listener : listeners) {
- listener.onRepoLoaded();
- }
- if (updateRemoteRepo) loadRemoteData();
- }
- }
-
- synchronized private void updateLatestVersion(OnlineModule[] onlineModules, String channel) {
- repoLoaded = false;
- Map versions = new ConcurrentHashMap<>();
- for (var module : onlineModules) {
- String release = module.getLatestRelease();
- if (channel.equals(channels[1]) && module.getLatestBetaRelease() != null && !module.getLatestBetaRelease().isEmpty()) {
- release = module.getLatestBetaRelease();
- } else if (channel.equals(channels[2])) {
- if (module.getLatestSnapshotRelease() != null && !module.getLatestSnapshotRelease().isEmpty())
- release = module.getLatestSnapshotRelease();
- else if (module.getLatestBetaRelease() != null && !module.getLatestBetaRelease().isEmpty())
- release = module.getLatestBetaRelease();
- }
- if (release == null || release.isEmpty()) continue;
- var splits = release.split("-", 2);
- if (splits.length < 2) continue;
- long verCode;
- String verName;
- try {
- verCode = Long.parseLong(splits[0]);
- verName = splits[1];
- } catch (NumberFormatException ignored) {
- continue;
- }
- String pkgName = module.getName();
- versions.put(pkgName, new ModuleVersion(verCode, verName));
- }
- latestVersion = versions;
- repoLoaded = true;
- for (RepoListener listener : listeners) {
- listener.onRepoLoaded();
- }
- }
-
- public void updateLatestVersion(String channel) {
- if (repoLoaded)
- updateLatestVersion(onlineModules.keySet().parallelStream().map(onlineModules::get).toArray(OnlineModule[]::new), channel);
- }
-
- @Nullable
- public ModuleVersion getModuleLatestVersion(String packageName) {
- return repoLoaded ? latestVersion.getOrDefault(packageName, null) : null;
- }
-
- @Nullable
- public List getReleases(String packageName) {
- var channel = App.getPreferences().getString("update_channel", channels[0]);
- List releases = new ArrayList<>();
- if (repoLoaded) {
- var module = onlineModules.get(packageName);
- if (module != null) {
- releases = module.getReleases();
- if (!module.releasesLoaded) {
- if (channel.equals(channels[1]) && !(module.getBetaReleases() != null && module.getBetaReleases().isEmpty())) {
- releases = module.getBetaReleases();
- } else if (channel.equals(channels[2]))
- if (!(module.getSnapshotReleases() != null && module.getSnapshotReleases().isEmpty()))
- releases = module.getSnapshotReleases();
- else if (!(module.getBetaReleases() != null && module.getBetaReleases().isEmpty()))
- releases = module.getBetaReleases();
- }
- }
- }
- return releases;
- }
-
- @Nullable
- public String getLatestReleaseTime(String packageName, String channel) {
- String releaseTime = null;
- if (repoLoaded) {
- var module = onlineModules.get(packageName);
- if (module != null) {
- releaseTime = module.getLatestReleaseTime();
- if (channel.equals(channels[1]) && module.getLatestBetaReleaseTime() != null) {
- releaseTime = module.getLatestBetaReleaseTime();
- } else if (channel.equals(channels[2]))
- if (module.getLatestSnapshotReleaseTime() != null)
- releaseTime = module.getLatestSnapshotReleaseTime();
- else if (module.getLatestBetaReleaseTime() != null)
- releaseTime = module.getLatestBetaReleaseTime();
- }
- }
- return releaseTime;
- }
-
- public void loadRemoteReleases(String packageName) {
- loadRemoteReleases(packageName, 0);
- }
-
- private void loadRemoteReleases(String packageName, int attempt) {
- if (attempt >= detailRepoUrls.length) {
- // Every detail mirror failed; fall back to the module's GitHub repo
- // so we can at least recover the README instead of failing outright.
- Log.w(App.TAG, "repo: detail mirrors exhausted for " + packageName + ", falling back to GitHub README");
- loadReadmeFromGithub(packageName, null, new IOException("All module detail mirrors failed for " + packageName));
- return;
- }
- String candidateRepoUrl = detailRepoUrls[attempt];
- Log.i(App.TAG, "repo: loadRemoteReleases " + packageName + " attempt " + attempt + " -> " + candidateRepoUrl);
- App.getOkHttpClient().newCall(new Request.Builder().url(String.format(candidateRepoUrl + "module/%s.json", packageName)).build()).enqueue(new Callback() {
- @Override
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
- Log.w(App.TAG, "repo: detail fetch failed for " + packageName + " from " + call.request().url() + ": " + e.getMessage());
- loadRemoteReleases(packageName, attempt + 1);
- }
-
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (!response.isSuccessful()) {
- Log.w(App.TAG, "repo: detail unexpected response " + response.code() + " for " + packageName + " from " + call.request().url());
- response.close();
- loadRemoteReleases(packageName, attempt + 1);
- return;
- }
- OnlineModule module;
- try (response) {
- ResponseBody body = response.body();
- if (body == null) {
- throw new IOException("Empty response from " + call.request().url());
- }
- String bodyString = body.string();
- if (bodyString.trim().isEmpty()) {
- throw new IOException("Empty response from " + call.request().url());
- }
- Gson gson = new Gson();
- module = gson.fromJson(bodyString, OnlineModule.class);
- if (module == null) {
- throw new IOException("Invalid response from " + call.request().url());
- }
- module.releasesLoaded = true;
- onlineModules.replace(packageName, module);
- } catch (Throwable t) {
- Log.e(App.TAG, "repo: detail parse failed for " + packageName, t);
- loadRemoteReleases(packageName, attempt + 1);
- return;
- }
- int releaseCount = module.getReleases() == null ? 0 : module.getReleases().size();
- Log.i(App.TAG, "repo: detail loaded for " + packageName + " from " + candidateRepoUrl + ", hasReadme=" + hasReadme(module) + ", releases=" + releaseCount);
- if (hasReadme(module)) {
- for (RepoListener listener : listeners) {
- listener.onModuleReleasesLoaded(module);
- }
- } else {
- // Detail loaded but carries no README; enrich it from GitHub
- // before publishing so the README tab is not shown as empty.
- Log.i(App.TAG, "repo: " + packageName + " detail has no README, fetching from GitHub");
- loadReadmeFromGithub(packageName, module, null);
- }
- }
- });
- }
-
- private boolean hasReadme(@Nullable OnlineModule module) {
- return module != null && ((module.getReadmeHTML() != null && !module.getReadmeHTML().isEmpty())
- || (module.getReadme() != null && !module.getReadme().isEmpty()));
- }
-
- // Fetches the module's rendered README from its GitHub repo. When `loaded`
- // is non-null the detail JSON already succeeded (valid releases, README is a
- // bonus) and is always published; when null, the JSON mirrors were
- // unreachable, so we enrich the cached summary and surface `error` only if
- // even the GitHub fetch fails.
- private void loadReadmeFromGithub(String packageName, @Nullable OnlineModule loaded, @Nullable Throwable error) {
- OnlineModule target = loaded != null ? loaded : onlineModules.get(packageName);
- if (target == null) {
- Log.w(App.TAG, "repo: no cached module to enrich for " + packageName + ", giving up");
- if (error != null) {
- for (RepoListener listener : listeners) {
- listener.onThrowable(error);
- }
- }
- return;
- }
- App.getOkHttpClient().newCall(new Request.Builder()
- .url(String.format(moduleGithubReadmeUrl, packageName))
- .header("Accept", "application/vnd.github.html+json")
- .build()).enqueue(new Callback() {
- @Override
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
- Log.w(App.TAG, "repo: GitHub README fetch failed for " + packageName + ": " + e.getMessage());
- publishReadmeFallback(packageName, target, null, loaded != null, error);
- }
-
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- String html = null;
- try (response) {
- if (response.isSuccessful()) {
- ResponseBody body = response.body();
- if (body != null) {
- String bodyString = body.string();
- if (!bodyString.trim().isEmpty()) {
- html = bodyString;
- }
- }
- } else {
- Log.w(App.TAG, "repo: GitHub README unexpected response " + response.code() + " for " + packageName);
- }
- } catch (Throwable t) {
- Log.e(App.TAG, "repo: GitHub README read failed for " + packageName, t);
- }
- Log.i(App.TAG, "repo: GitHub README for " + packageName + " -> " + (html != null ? "recovered (" + html.length() + " bytes)" : "unavailable"));
- publishReadmeFallback(packageName, target, html, loaded != null, error);
- }
- });
- }
-
- private void publishReadmeFallback(String packageName, OnlineModule module, @Nullable String readmeHTML, boolean detailLoaded, @Nullable Throwable error) {
- if (readmeHTML != null) {
- module.setReadmeHTML(readmeHTML);
- onlineModules.replace(packageName, module);
- }
- if (detailLoaded || readmeHTML != null) {
- // The releases are already valid, or we recovered a README: publish
- // the (possibly enriched) module to the UI.
- Log.i(App.TAG, "repo: publishing " + packageName + " (detailLoaded=" + detailLoaded + ", readmeRecovered=" + (readmeHTML != null) + ")");
- for (RepoListener listener : listeners) {
- listener.onModuleReleasesLoaded(module);
- }
- } else if (error != null) {
- Log.w(App.TAG, "repo: nothing to publish for " + packageName + ", reporting failure");
- for (RepoListener listener : listeners) {
- listener.onThrowable(error);
- }
- }
- }
-
- public void addListener(RepoListener listener) {
- listeners.add(listener);
- }
-
- public void removeListener(RepoListener listener) {
- listeners.remove(listener);
- }
-
- @Nullable
- public OnlineModule getOnlineModule(String packageName) {
- return repoLoaded && packageName != null ? onlineModules.get(packageName) : null;
- }
-
- @Nullable
- public Collection getOnlineModules() {
- return repoLoaded ? onlineModules.values() : null;
- }
-
- public interface RepoListener {
- default void onRepoLoaded() {
- }
-
- default void onModuleReleasesLoaded(OnlineModule module) {
- }
-
- default void onThrowable(Throwable t) {
- Log.e(App.TAG, "load repo failed", t);
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/repo/model/Collaborator.java b/app/src/main/java/org/lsposed/manager/repo/model/Collaborator.java
deleted file mode 100644
index bd749d7d3..000000000
--- a/app/src/main/java/org/lsposed/manager/repo/model/Collaborator.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.repo.model;
-
-import androidx.annotation.Nullable;
-
-import com.google.gson.annotations.Expose;
-import com.google.gson.annotations.SerializedName;
-
-public class Collaborator {
-
- @SerializedName("login")
- @Expose
- private String login;
- @SerializedName("name")
- @Expose
- private String name;
-
- @Nullable
- public String getLogin() {
- return login;
- }
-
- public void setLogin(String login) {
- this.login = login;
- }
-
- @Nullable
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/repo/model/OnlineModule.java b/app/src/main/java/org/lsposed/manager/repo/model/OnlineModule.java
deleted file mode 100644
index 2c11f6959..000000000
--- a/app/src/main/java/org/lsposed/manager/repo/model/OnlineModule.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.repo.model;
-
-import androidx.annotation.Nullable;
-
-import com.google.gson.annotations.Expose;
-import com.google.gson.annotations.SerializedName;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class OnlineModule {
-
- @SerializedName("name")
- @Expose
- private String name;
- @SerializedName("description")
- @Expose
- private String description;
- @SerializedName("url")
- @Expose
- private String url;
- @SerializedName("homepageUrl")
- @Expose
- private String homepageUrl;
- @SerializedName("collaborators")
- @Expose
- private List collaborators = new ArrayList<>();
- @SerializedName("latestRelease")
- @Expose
- private String latestRelease;
- @SerializedName("latestReleaseTime")
- @Expose
- private String latestReleaseTime;
- @SerializedName("latestBetaRelease")
- @Expose
- private String latestBetaRelease;
- @SerializedName("latestBetaReleaseTime")
- @Expose
- private String latestBetaReleaseTime;
- @SerializedName("latestSnapshotRelease")
- @Expose
- private String latestSnapshotRelease;
- @SerializedName("latestSnapshotReleaseTime")
- @Expose
- private String latestSnapshotReleaseTime;
- @SerializedName("releases")
- @Expose
- private List releases = new ArrayList<>();
- @SerializedName("betaReleases")
- @Expose
- private final List betaReleases = new ArrayList<>();
- @SerializedName("snapshotReleases")
- @Expose
- private final List snapshotReleases = new ArrayList<>();
- @SerializedName("readme")
- @Expose
- private String readme;
- @SerializedName("readmeHTML")
- @Expose
- private String readmeHTML;
- @SerializedName("summary")
- @Expose
- private String summary;
- @SerializedName("scope")
- @Expose
- private List scope = new ArrayList<>();
- @SerializedName("sourceUrl")
- @Expose
- private String sourceUrl;
- @SerializedName("hide")
- @Expose
- private Boolean hide;
- @SerializedName("additionalAuthors")
- @Expose
- private List additionalAuthors = null;
- @SerializedName("updatedAt")
- @Expose
- private String updatedAt;
- @SerializedName("createdAt")
- @Expose
- private String createdAt;
- @SerializedName("stargazerCount")
- @Expose
- private Integer stargazerCount;
- public boolean releasesLoaded = false;
-
- @Nullable
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- @Nullable
- public String getDescription() {
- return description;
- }
-
- public void setDescription(String description) {
- this.description = description;
- }
-
- @Nullable
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-
- @Nullable
- public String getHomepageUrl() {
- return homepageUrl;
- }
-
- public void setHomepageUrl(String homepageUrl) {
- this.homepageUrl = homepageUrl;
- }
-
- @Nullable
- public List getCollaborators() {
- return collaborators;
- }
-
- public void setCollaborators(List collaborators) {
- this.collaborators = collaborators;
- }
-
- @Nullable
- public List getReleases() {
- return releases;
- }
-
- @Nullable
- public String getLatestReleaseTime() {
- return latestReleaseTime;
- }
-
- public void setReleases(List releases) {
- this.releases = releases;
- }
-
- @Nullable
- public String getReadme() {
- return readme;
- }
-
- public void setReadme(String readme) {
- this.readme = readme;
- }
-
- @Nullable
- public String getReadmeHTML() {
- return readmeHTML;
- }
-
- public void setReadmeHTML(String readmeHTML) {
- this.readmeHTML = readmeHTML;
- }
-
- @Nullable
- public String getSummary() {
- return summary;
- }
-
- public void setSummary(String summary) {
- this.summary = summary;
- }
-
- @Nullable
- public List getScope() {
- return scope;
- }
-
- public void setScope(List scope) {
- this.scope = scope;
- }
-
- @Nullable
- public String getSourceUrl() {
- return sourceUrl;
- }
-
- public void setSourceUrl(String sourceUrl) {
- this.sourceUrl = sourceUrl;
- }
-
- public Boolean isHide() {
- return hide;
- }
-
- public void setHide(Boolean hide) {
- this.hide = hide;
- }
-
- @Nullable
- public List getAdditionalAuthors() {
- return additionalAuthors;
- }
-
- public void setAdditionalAuthors(List additionalAuthors) {
- this.additionalAuthors = additionalAuthors;
- }
-
- @Nullable
- public String getUpdatedAt() {
- return updatedAt;
- }
-
- public void setUpdatedAt(String updatedAt) {
- this.updatedAt = updatedAt;
- }
-
- @Nullable
- public String getCreatedAt() {
- return createdAt;
- }
-
- public void setCreatedAt(String createdAt) {
- this.createdAt = createdAt;
- }
-
- @Nullable
- public Integer getStargazerCount() {
- return stargazerCount;
- }
-
- public void setStargazerCount(Integer stargazerCount) {
- this.stargazerCount = stargazerCount;
- }
-
- @Nullable
- public String getLatestRelease() {
- return latestRelease;
- }
-
- public void setLatestRelease(String latestRelease) {
- this.latestRelease = latestRelease;
- }
-
- @Nullable
- public String getLatestBetaRelease() {
- return latestBetaRelease;
- }
-
- @Nullable
- public String getLatestBetaReleaseTime() {
- return latestBetaReleaseTime;
- }
-
- @Nullable
- public String getLatestSnapshotRelease() {
- return latestSnapshotRelease;
- }
-
- @Nullable
- public String getLatestSnapshotReleaseTime() {
- return latestSnapshotReleaseTime;
- }
-
- @Nullable
- public List getBetaReleases() {
- return betaReleases;
- }
-
- @Nullable
- public List getSnapshotReleases() {
- return snapshotReleases;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/repo/model/Release.java b/app/src/main/java/org/lsposed/manager/repo/model/Release.java
deleted file mode 100644
index 57d248060..000000000
--- a/app/src/main/java/org/lsposed/manager/repo/model/Release.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.repo.model;
-
-import androidx.annotation.Nullable;
-
-import com.google.gson.annotations.Expose;
-import com.google.gson.annotations.SerializedName;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class Release {
-
- @SerializedName("name")
- @Expose
- private String name;
- @SerializedName("url")
- @Expose
- private String url;
- @SerializedName("description")
- @Expose
- private String description;
- @SerializedName("descriptionHTML")
- @Expose
- private String descriptionHTML;
- @SerializedName("createdAt")
- @Expose
- private String createdAt;
- @SerializedName("publishedAt")
- @Expose
- private String publishedAt;
- @SerializedName("updatedAt")
- @Expose
- private String updatedAt;
- @SerializedName("tagName")
- @Expose
- private String tagName;
- @SerializedName("isPrerelease")
- @Expose
- private Boolean isPrerelease;
- @SerializedName("releaseAssets")
- @Expose
- private List releaseAssets = new ArrayList<>();
-
- @Nullable
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- @Nullable
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-
- @Nullable
- public String getDescription() {
- return description;
- }
-
- public void setDescription(String description) {
- this.description = description;
- }
-
- @Nullable
- public String getDescriptionHTML() {
- return descriptionHTML;
- }
-
- public void setDescriptionHTML(String descriptionHTML) {
- this.descriptionHTML = descriptionHTML;
- }
-
- @Nullable
- public String getCreatedAt() {
- return createdAt;
- }
-
- public void setCreatedAt(String createdAt) {
- this.createdAt = createdAt;
- }
-
- @Nullable
- public String getPublishedAt() {
- return publishedAt;
- }
-
- public void setPublishedAt(String publishedAt) {
- this.publishedAt = publishedAt;
- }
-
- @Nullable
- public String getUpdatedAt() {
- return updatedAt;
- }
-
- public void setUpdatedAt(String updatedAt) {
- this.updatedAt = updatedAt;
- }
-
- @Nullable
- public String getTagName() {
- return tagName;
- }
-
- public void setTagName(String tagName) {
- this.tagName = tagName;
- }
-
- @Nullable
- public Boolean getIsPrerelease() {
- return isPrerelease;
- }
-
- public void setIsPrerelease(Boolean isPrerelease) {
- this.isPrerelease = isPrerelease;
- }
-
- @Nullable
- public List getReleaseAssets() {
- return releaseAssets;
- }
-
- public void setReleaseAssets(List releaseAssets) {
- this.releaseAssets = releaseAssets;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/repo/model/ReleaseAsset.java b/app/src/main/java/org/lsposed/manager/repo/model/ReleaseAsset.java
deleted file mode 100644
index 773fe1a30..000000000
--- a/app/src/main/java/org/lsposed/manager/repo/model/ReleaseAsset.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.repo.model;
-
-import androidx.annotation.Nullable;
-
-import com.google.gson.annotations.Expose;
-import com.google.gson.annotations.SerializedName;
-
-public class ReleaseAsset {
-
- @SerializedName("name")
- @Expose
- private String name;
- @SerializedName("contentType")
- @Expose
- private String contentType;
- @SerializedName("downloadUrl")
- @Expose
- private String downloadUrl;
- @SerializedName("downloadCount")
- @Expose
- private int downloadCount = 0;
- @SerializedName("size")
- @Expose
- private int size = 0;
-
- @Nullable
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- @Nullable
- public String getContentType() {
- return contentType;
- }
-
- public void setContentType(String contentType) {
- this.contentType = contentType;
- }
-
- @Nullable
- public String getDownloadUrl() {
- return downloadUrl;
- }
-
- public void setDownloadUrl(String downloadUrl) {
- this.downloadUrl = downloadUrl;
- }
-
- public int getDownloadCount() {
- return downloadCount;
- }
-
- public void setDownloadCount(int downloadCount) {
- this.downloadCount = downloadCount;
- }
-
- public int getSize() {
- return size;
- }
-
- public void setSize(int size) {
- this.size = size;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/activity/MainActivity.java b/app/src/main/java/org/lsposed/manager/ui/activity/MainActivity.java
deleted file mode 100644
index 20f19fddb..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/activity/MainActivity.java
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.activity;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
-import androidx.annotation.NonNull;
-import androidx.navigation.NavController;
-import androidx.navigation.NavOptions;
-import androidx.navigation.Navigation;
-import androidx.navigation.fragment.NavHostFragment;
-import androidx.navigation.ui.NavigationUI;
-
-import com.google.android.material.navigation.NavigationBarView;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.ActivityMainBinding;
-import org.lsposed.manager.repo.RepoLoader;
-import org.lsposed.manager.ui.activity.base.BaseActivity;
-import org.lsposed.manager.util.ModuleUtil;
-import org.lsposed.manager.util.ShortcutUtil;
-import org.lsposed.manager.util.UpdateUtil;
-
-import java.util.HashSet;
-import java.util.Objects;
-
-import rikka.core.util.ResourceUtils;
-
-public class MainActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener {
- private static final String KEY_PREFIX = MainActivity.class.getName() + '.';
- private static final String EXTRA_SAVED_INSTANCE_STATE = KEY_PREFIX + "SAVED_INSTANCE_STATE";
-
- private static final RepoLoader repoLoader = RepoLoader.getInstance();
- private static final ModuleUtil moduleUtil = ModuleUtil.getInstance();
-
- private boolean restarting;
- private ActivityMainBinding binding;
-
- @NonNull
- public static Intent newIntent(@NonNull Context context) {
- return new Intent(context, MainActivity.class);
- }
-
- @NonNull
- private static Intent newIntent(@NonNull Bundle savedInstanceState, @NonNull Context context) {
- return newIntent(context)
- .putExtra(EXTRA_SAVED_INSTANCE_STATE, savedInstanceState);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- if (savedInstanceState == null) {
- savedInstanceState = getIntent().getBundleExtra(EXTRA_SAVED_INSTANCE_STATE);
- }
- super.onCreate(savedInstanceState);
-
- binding = ActivityMainBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- repoLoader.addListener(this);
- moduleUtil.addListener(this);
-
- onModulesReloaded();
-
- NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
- if (navHostFragment == null) {
- return;
- }
-
- NavController navController = navHostFragment.getNavController();
- var nav = (NavigationBarView) binding.nav;
- NavigationUI.setupWithNavController(nav, navController);
-
- handleIntent(getIntent());
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- handleIntent(intent);
- }
-
- private void handleIntent(Intent intent) {
- if (intent == null) {
- return;
- }
- NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
- if (navHostFragment == null) {
- return;
- }
- NavController navController = navHostFragment.getNavController();
- var nav = (NavigationBarView) binding.nav;
- if (intent.getAction() != null && intent.getAction().equals("android.intent.action.APPLICATION_PREFERENCES")) {
- nav.setSelectedItemId(R.id.settings_fragment);
- } else if (ConfigManager.isBinderAlive()) {
- if (!TextUtils.isEmpty(intent.getDataString())) {
- switch (intent.getDataString()) {
- case "modules" -> nav.setSelectedItemId(R.id.modules_nav);
- case "logs" -> nav.setSelectedItemId(R.id.logs_fragment);
- case "repo" -> {
- if (ConfigManager.isMagiskInstalled()) {
- nav.setSelectedItemId(R.id.repo_nav);
- }
- }
- case "settings" -> nav.setSelectedItemId(R.id.settings_fragment);
- default -> {
- var data = intent.getData();
- if (data != null && Objects.equals(data.getScheme(), "module")) {
- navController.navigate(
- new Uri.Builder().scheme("lsposed").authority("module").appendQueryParameter("modulePackageName", data.getHost()).appendQueryParameter("moduleUserId", String.valueOf(data.getPort())).build(),
- new NavOptions.Builder().setEnterAnim(R.anim.fragment_enter).setExitAnim(R.anim.fragment_exit).setPopEnterAnim(R.anim.fragment_enter_pop).setPopExitAnim(R.anim.fragment_exit_pop).setLaunchSingleTop(true).setPopUpTo(navController.getGraph().getStartDestinationId(), false, true).build());
- }
- }
- }
- }
- }
- }
-
- @Override
- public boolean onSupportNavigateUp() {
- NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
- return navController.navigateUp() || super.onSupportNavigateUp();
- }
-
- public void restart() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || App.isParasitic) {
- recreate();
- } else {
- try {
- Bundle savedInstanceState = new Bundle();
- onSaveInstanceState(savedInstanceState);
- finish();
- startActivity(newIntent(savedInstanceState, this));
- overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
- restarting = true;
- } catch (Throwable e) {
- recreate();
- }
- }
- }
-
- @Override
- public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
- return restarting || super.dispatchKeyEvent(event);
- }
-
- @SuppressLint("RestrictedApi")
- @Override
- public boolean dispatchKeyShortcutEvent(@NonNull KeyEvent event) {
- return restarting || super.dispatchKeyShortcutEvent(event);
- }
-
- @Override
- public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
- return restarting || super.dispatchTouchEvent(event);
- }
-
- @Override
- public boolean dispatchTrackballEvent(@NonNull MotionEvent event) {
- return restarting || super.dispatchTrackballEvent(event);
- }
-
- @Override
- public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
- return restarting || super.dispatchGenericMotionEvent(event);
- }
-
-
- @Override
- public void onRepoLoaded() {
- final int[] count = new int[]{0};
- HashSet processedModules = new HashSet<>();
- var modules = moduleUtil.getModules();
- if (modules == null) return;
- modules.forEach((k, v) -> {
- if (!processedModules.contains(k.first)) {
- var ver = repoLoader.getModuleLatestVersion(k.first);
- if (ver != null && ver.upgradable(v.versionCode, v.versionName)) {
- ++count[0];
- }
- processedModules.add(k.first);
- }
- }
- );
- runOnUiThread(() -> {
- if (count[0] > 0 && binding != null) {
- var nav = (NavigationBarView) binding.nav;
- var badge = nav.getOrCreateBadge(R.id.repo_nav);
- badge.setVisible(true);
- badge.setNumber(count[0]);
- } else {
- onThrowable(null);
- }
- });
- }
-
- @Override
- public void onThrowable(Throwable t) {
- runOnUiThread(() -> {
- if (binding != null) {
- var nav = (NavigationBarView) binding.nav;
- var badge = nav.getOrCreateBadge(R.id.repo_nav);
- badge.setVisible(false);
- }
- });
- }
-
- @Override
- public void onModulesReloaded() {
- onRepoLoaded();
- setModulesSummary(moduleUtil.getEnabledModulesCount());
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (ConfigManager.isBinderAlive()) {
- setModulesSummary(moduleUtil.getEnabledModulesCount());
- } else setModulesSummary(0);
- if (binding != null) {
- var nav = (NavigationBarView) binding.nav;
- if (UpdateUtil.needUpdate()) {
- var badge = nav.getOrCreateBadge(R.id.main_fragment);
- badge.setVisible(true);
- }
-
- if (!ConfigManager.isBinderAlive()) {
- nav.getMenu().removeItem(R.id.logs_fragment);
- nav.getMenu().removeItem(R.id.modules_nav);
- if (!ConfigManager.isMagiskInstalled()) {
- nav.getMenu().removeItem(R.id.repo_nav);
- }
- }
- }
- if (App.isParasitic) {
- var updateShortcut = ShortcutUtil.updateShortcut();
- Log.d(App.TAG, "update shortcut success = " + updateShortcut);
- }
- }
-
- private void setModulesSummary(int moduleCount) {
- runOnUiThread(() -> {
- if (binding != null) {
- var nav = (NavigationBarView) binding.nav;
- var badge = nav.getOrCreateBadge(R.id.modules_nav);
- badge.setBackgroundColor(ResourceUtils.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary));
- badge.setBadgeTextColor(ResourceUtils.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnPrimary));
- if (moduleCount > 0) {
- badge.setVisible(true);
- badge.setNumber(moduleCount);
- } else {
- badge.setVisible(false);
- }
- }
- });
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- repoLoader.removeListener(this);
- moduleUtil.removeListener(this);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/activity/base/BaseActivity.java b/app/src/main/java/org/lsposed/manager/ui/activity/base/BaseActivity.java
deleted file mode 100644
index e0453f0be..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/activity/base/BaseActivity.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.activity.base;
-
-import android.app.ActivityManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.BitmapDrawable;
-import android.os.Bundle;
-import android.view.Window;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.R;
-import org.lsposed.manager.util.ThemeUtil;
-
-import rikka.material.app.MaterialActivity;
-
-public class BaseActivity extends MaterialActivity {
- private static Bitmap icon = null;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- setTheme(R.style.AppTheme);
- super.onCreate(savedInstanceState);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- if (!App.isParasitic) return;
- for (var task : getSystemService(ActivityManager.class).getAppTasks()) {
- task.setExcludeFromRecents(false);
- }
- if (icon == null) {
- var drawable = getApplicationInfo().loadIcon(getPackageManager());
- if (drawable instanceof BitmapDrawable) {
- icon = ((BitmapDrawable) drawable).getBitmap();
- } else if (drawable instanceof AdaptiveIconDrawable) {
- icon = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(icon);
- drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- drawable.draw(canvas);
- }
- }
- setTaskDescription(new ActivityManager.TaskDescription(getTitle().toString(), icon, getColor(R.color.ic_launcher_background)));
- }
-
- @Override
- public void onApplyUserThemeResource(@NonNull Resources.Theme theme, boolean isDecorView) {
- if (!ThemeUtil.isSystemAccent()) {
- theme.applyStyle(ThemeUtil.getColorThemeStyleRes(), true);
- }
- theme.applyStyle(ThemeUtil.getNightThemeStyleRes(this), true);
- theme.applyStyle(rikka.material.preference.R.style.ThemeOverlay_Rikka_Material3_Preference, true);
- }
-
- @Override
- public String computeUserThemeKey() {
- return ThemeUtil.getColorTheme() + ThemeUtil.getNightTheme(this);
- }
-
- @Override
- public void onApplyTranslucentSystemBars() {
- super.onApplyTranslucentSystemBars();
- Window window = getWindow();
- window.setStatusBarColor(Color.TRANSPARENT);
- window.setNavigationBarColor(Color.TRANSPARENT);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/dialog/BlurBehindDialogBuilder.java b/app/src/main/java/org/lsposed/manager/ui/dialog/BlurBehindDialogBuilder.java
deleted file mode 100644
index a559b1f06..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/dialog/BlurBehindDialogBuilder.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.dialog;
-
-import android.animation.ValueAnimator;
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.os.Build;
-import android.util.Log;
-import android.view.SurfaceControl;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.animation.DecelerateInterpolator;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import org.lsposed.manager.App;
-
-import java.lang.reflect.Method;
-import java.util.function.Consumer;
-
-@SuppressWarnings({"JavaReflectionMemberAccess", "ConstantConditions"})
-public class BlurBehindDialogBuilder extends MaterialAlertDialogBuilder {
- private static final boolean supportBlur = getSystemProperty("ro.surface_flinger.supports_background_blur", false) && !getSystemProperty("persist.sys.sf.disable_blurs", false);
-
- public BlurBehindDialogBuilder(@NonNull Context context) {
- super(context);
- }
-
- public BlurBehindDialogBuilder(@NonNull Context context, int overrideThemeResId) {
- super(context, overrideThemeResId);
- }
-
- @NonNull
- @Override
- public AlertDialog create() {
- AlertDialog dialog = super.create();
- setupWindowBlurListener(dialog);
- return dialog;
- }
-
- private void setupWindowBlurListener(AlertDialog dialog) {
- var window = dialog.getWindow();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
- Consumer windowBlurEnabledListener = enabled -> updateWindowForBlurs(window, enabled);
- window.getDecorView().addOnAttachStateChangeListener(
- new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(@NonNull View v) {
- window.getWindowManager().addCrossWindowBlurEnabledListener(
- windowBlurEnabledListener);
- }
-
- @Override
- public void onViewDetachedFromWindow(@NonNull View v) {
- window.getWindowManager().removeCrossWindowBlurEnabledListener(
- windowBlurEnabledListener);
- }
- });
- } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
- dialog.setOnShowListener(d -> updateWindowForBlurs(window, supportBlur));
- }
- }
-
- private void updateWindowForBlurs(Window window, boolean blursEnabled) {
- float mDimAmountWithBlur = 0.1f;
- float mDimAmountNoBlur = 0.32f;
- window.setDimAmount(blursEnabled ?
- mDimAmountWithBlur : mDimAmountNoBlur);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- window.getAttributes().setBlurBehindRadius(20);
- window.setAttributes(window.getAttributes());
- } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
- if (blursEnabled) {
- View view = window.getDecorView();
- ValueAnimator animator = ValueAnimator.ofInt(1, 53);
- animator.setInterpolator(new DecelerateInterpolator());
- try {
- Object viewRootImpl = view.getClass().getMethod("getViewRootImpl").invoke(view);
- if (viewRootImpl == null) {
- return;
- }
- SurfaceControl surfaceControl = (SurfaceControl) viewRootImpl.getClass().getMethod("getSurfaceControl").invoke(viewRootImpl);
-
- @SuppressLint("BlockedPrivateApi") Method setBackgroundBlurRadius = SurfaceControl.Transaction.class.getDeclaredMethod("setBackgroundBlurRadius", SurfaceControl.class, int.class);
- animator.addUpdateListener(animation -> {
- try {
- SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
- var animatedValue = animation.getAnimatedValue();
- if (animatedValue != null) {
- setBackgroundBlurRadius.invoke(transaction, surfaceControl, (int) animatedValue);
- }
- transaction.apply();
- } catch (Throwable t) {
- Log.e(App.TAG, "Blur behind dialog builder", t);
- }
- });
- } catch (Throwable t) {
- Log.e(App.TAG, "Blur behind dialog builder", t);
- }
- view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(@NonNull View v) {
- }
-
- @Override
- public void onViewDetachedFromWindow(@NonNull View v) {
- animator.cancel();
- }
- });
- animator.start();
- }
- }
- }
-
- public static boolean getSystemProperty(String key, boolean defaultValue) {
- boolean value = defaultValue;
- try {
- Class> c = Class.forName("android.os.SystemProperties");
- Method get = c.getMethod("getBoolean", String.class, boolean.class);
- value = (boolean) get.invoke(c, key, defaultValue);
- } catch (Exception e) {
- Log.e(App.TAG, "Blur behind dialog builder get system property", e);
- }
- return value;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/dialog/WelcomeDialog.java b/app/src/main/java/org/lsposed/manager/ui/dialog/WelcomeDialog.java
deleted file mode 100644
index 09ed378e9..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/dialog/WelcomeDialog.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2022 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.dialog;
-
-import android.app.Dialog;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentManager;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-import org.lsposed.manager.ui.fragment.BaseFragment;
-import org.lsposed.manager.util.ShortcutUtil;
-
-public class WelcomeDialog extends DialogFragment {
- private static boolean shown = false;
-
- private Dialog parasiticDialog(BlurBehindDialogBuilder builder) {
- var shortcutSupported = ShortcutUtil.isRequestPinShortcutSupported(requireContext());
- builder
- .setTitle(R.string.parasitic_welcome)
- .setMessage(shortcutSupported ? R.string.parasitic_welcome_summary :
- R.string.parasitic_welcome_summary_no_shortcut_support)
- .setNegativeButton(R.string.never_show, (dialog, which) ->
- App.getPreferences().edit().putBoolean("never_show_welcome", true).apply())
- .setPositiveButton(android.R.string.ok, null)
- .setNeutralButton(R.string.create_shortcut, (dialog, which) -> {
- var home = (BaseFragment) getParentFragment();
- if (!ShortcutUtil.requestPinLaunchShortcut(() -> {
- App.getPreferences().edit().putBoolean("never_show_welcome", true).apply();
- if (home != null) {
- home.showHint(R.string.settings_shortcut_pinned_hint, false);
- }
- })) {
- if (home != null) {
- home.showHint(R.string.settings_unsupported_pin_shortcut_summary, false);
- }
- }
- });
- return builder.create();
- }
-
- private Dialog appDialog(BlurBehindDialogBuilder builder) {
-
- return builder
- .setTitle(R.string.app_welcome)
- .setMessage(R.string.app_welcome_summary)
- .setNegativeButton(R.string.never_show, (d, w) ->
- App.getPreferences().edit().putBoolean("never_show_welcome", true).apply())
- .setPositiveButton(android.R.string.ok, null)
- .create();
- }
-
- @NonNull
- @Override
- public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
- var builder = new BlurBehindDialogBuilder(requireContext(),
- R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons);
- if (App.isParasitic) {
- return parasiticDialog(builder);
- } else {
- return appDialog(builder);
- }
- }
-
- public static void showIfNeed(FragmentManager fm) {
- if (shown) return;
- if (!ConfigManager.isBinderAlive() ||
- App.getPreferences().getBoolean("never_show_welcome", false) ||
- (App.isParasitic && ShortcutUtil.isLaunchShortcutPinned())) {
- shown = true;
- return;
- }
- new WelcomeDialog().show(fm, "welcome");
- shown = true;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/AppListFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/AppListFragment.java
deleted file mode 100644
index e00a0b621..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/AppListFragment.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.activity.OnBackPressedCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.SearchView;
-import androidx.core.view.MenuProvider;
-import androidx.recyclerview.widget.ConcatAdapter;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-import org.lsposed.manager.adapters.AppHelper;
-import org.lsposed.manager.adapters.ScopeAdapter;
-import org.lsposed.manager.databinding.FragmentAppListBinding;
-import org.lsposed.manager.util.BackupUtils;
-import org.lsposed.manager.util.ModuleUtil;
-
-import rikka.material.app.LocaleDelegate;
-import rikka.recyclerview.RecyclerViewKt;
-
-public class AppListFragment extends BaseFragment implements MenuProvider {
-
- public SearchView searchView;
- private ScopeAdapter scopeAdapter;
- private ModuleUtil.InstalledModule module;
-
- private SearchView.OnQueryTextListener searchListener;
- public FragmentAppListBinding binding;
- public ActivityResultLauncher backupLauncher;
- public ActivityResultLauncher restoreLauncher;
-
- private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
- @Override
- public void onChanged() {
- if (binding != null && scopeAdapter != null) {
- binding.swipeRefreshLayout.setRefreshing(!scopeAdapter.isLoaded());
- }
- }
- };
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = FragmentAppListBinding.inflate(getLayoutInflater(), container, false);
- if (module == null) {
- return binding.getRoot();
- }
- binding.appBar.setLiftable(true);
- String title;
- if (module.userId != 0) {
- title = String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", module.getAppName(), module.userId);
- } else {
- title = module.getAppName();
- }
- binding.toolbar.setSubtitle(module.packageName);
-
- scopeAdapter = new ScopeAdapter(this, module);
- scopeAdapter.setHasStableIds(true);
- scopeAdapter.registerAdapterDataObserver(observer);
- var concatAdapter = new ConcatAdapter();
- concatAdapter.addAdapter(scopeAdapter.switchAdaptor);
- concatAdapter.addAdapter(scopeAdapter);
- concatAdapter.addAdapter(scopeAdapter.footerAdaptor);
- binding.recyclerView.setAdapter(concatAdapter);
- binding.recyclerView.setHasFixedSize(true);
- binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
- binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top));
- RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
- binding.swipeRefreshLayout.setOnRefreshListener(() -> scopeAdapter.refresh(true));
- binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
- Intent intent = AppHelper.getSettingsIntent(module.packageName, module.userId);
- if (intent == null) {
- binding.fab.setVisibility(View.GONE);
- } else {
- binding.fab.setVisibility(View.VISIBLE);
- binding.fab.setOnClickListener(v -> ConfigManager.startActivityAsUserWithFeature(intent, module.userId));
- }
- searchListener = scopeAdapter.getSearchListener();
-
- setupToolbar(binding.toolbar, binding.clickView, title, R.menu.menu_app_list, view -> requireActivity().getOnBackPressedDispatcher().onBackPressed());
- View.OnClickListener l = v -> {
- if (searchView.isIconified()) {
- binding.recyclerView.smoothScrollToPosition(0);
- binding.appBar.setExpanded(true, true);
- }
- };
- binding.toolbar.setOnClickListener(l);
- binding.clickView.setOnClickListener(l);
-
- return binding.getRoot();
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- if (module == null) {
- if (!safeNavigate(R.id.action_app_list_fragment_to_modules_fragment)) {
- safeNavigate(R.id.modules_nav);
- }
- }
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- AppListFragmentArgs args = AppListFragmentArgs.fromBundle(getArguments());
- String modulePackageName = args.getModulePackageName();
- int moduleUserId = args.getModuleUserId();
-
- module = ModuleUtil.getInstance().getModule(modulePackageName, moduleUserId);
- if (module == null) {
- if (!safeNavigate(R.id.action_app_list_fragment_to_modules_fragment)) {
- safeNavigate(R.id.modules_nav);
- }
- }
-
- backupLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("application/gzip"),
- uri -> {
- if (uri == null) return;
- runAsync(() -> {
- try {
- BackupUtils.backup(uri, modulePackageName);
- } catch (Exception e) {
- var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage());
- showHint(text, false);
- }
- });
- });
- restoreLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(),
- uri -> {
- if (uri == null) return;
- runAsync(() -> {
- try {
- BackupUtils.restore(uri, modulePackageName);
- } catch (Exception e) {
- var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage());
- showHint(text, false);
- }
- });
- });
-
- requireActivity().getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
- @Override
- public void handleOnBackPressed() {
- scopeAdapter.onBackPressed();
- }
- });
- }
-
- @Override
- public void onResume() {
- super.onResume();
- if (scopeAdapter != null) scopeAdapter.refresh();
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- if (scopeAdapter != null) scopeAdapter.unregisterAdapterDataObserver(observer);
- binding = null;
- }
-
- @Override
- public boolean onMenuItemSelected(@NonNull MenuItem item) {
- return scopeAdapter.onOptionsItemSelected(item);
- }
-
- @Override
- public void onPrepareMenu(@NonNull Menu menu) {
- searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
- searchView.setOnQueryTextListener(searchListener);
- searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View arg0) {
- binding.appBar.setExpanded(false, true);
- binding.recyclerView.setNestedScrollingEnabled(false);
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- binding.recyclerView.setNestedScrollingEnabled(true);
- }
- });
- searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
- scopeAdapter.onPrepareOptionsMenu(menu);
- }
-
- @Override
- public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
-
- }
-
- @Override
- public boolean onContextItemSelected(@NonNull MenuItem item) {
- if (scopeAdapter.onContextItemSelected(item)) {
- return true;
- }
- return super.onContextItemSelected(item);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/BaseFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/BaseFragment.java
deleted file mode 100644
index c62ca2c6f..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/BaseFragment.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.view.View;
-import android.widget.Toast;
-
-import androidx.annotation.IdRes;
-import androidx.annotation.StringRes;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.view.MenuProvider;
-import androidx.fragment.app.Fragment;
-import androidx.navigation.NavController;
-import androidx.navigation.NavDirections;
-import androidx.navigation.NavOptions;
-import androidx.navigation.fragment.NavHostFragment;
-
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.android.material.snackbar.Snackbar;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.R;
-import org.lsposed.manager.util.AccessibilityUtils;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-import java.util.concurrent.FutureTask;
-
-public abstract class BaseFragment extends Fragment {
-
- public void navigateUp() {
- getNavController().navigateUp();
- }
-
- public NavController getNavController() {
- return NavHostFragment.findNavController(this);
- }
-
- public boolean safeNavigate(@IdRes int resId) {
- try {
- if (!AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver())) {
- var clearedNavOptions = new NavOptions.Builder().build();
- getNavController().navigate(resId, clearedNavOptions);
- } else {
- getNavController().navigate(resId);
- }
- return true;
- } catch (IllegalArgumentException ignored) {
- return false;
- }
- }
-
- public boolean safeNavigate(NavDirections direction) {
- try {
- if (!AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver())) {
- var clearedNavOptions = new NavOptions.Builder().build();
- getNavController().navigate(direction, clearedNavOptions);
- } else {
- getNavController().navigate(direction);
- }
- return true;
- } catch (IllegalArgumentException ignored) {
- return false;
- }
- }
-
- public void setupToolbar(Toolbar toolbar, View tipsView, int title) {
- setupToolbar(toolbar, tipsView, getString(title), -1);
- }
-
- public void setupToolbar(Toolbar toolbar, View tipsView, int title, int menu) {
- setupToolbar(toolbar, tipsView, getString(title), menu, null);
- }
-
- public void setupToolbar(Toolbar toolbar, View tipsView, String title, int menu) {
- setupToolbar(toolbar, tipsView, title, menu, null);
- }
-
- public void setupToolbar(Toolbar toolbar, View tipsView, String title, int menu, View.OnClickListener navigationOnClickListener) {
- toolbar.setNavigationOnClickListener(navigationOnClickListener == null ? (v -> navigateUp()) : navigationOnClickListener);
- toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24);
- toolbar.setTitle(title);
- toolbar.setTooltipText(title);
- if (tipsView != null) tipsView.setTooltipText(title);
- if (menu != -1) {
- toolbar.inflateMenu(menu);
- if (this instanceof MenuProvider self) {
- toolbar.setOnMenuItemClickListener(self::onMenuItemSelected);
- self.onPrepareMenu(toolbar.getMenu());
- }
- }
- }
-
- public void runAsync(Runnable runnable) {
- App.getExecutorService().submit(runnable);
- }
-
- public Future runAsync(Callable callable) {
- return App.getExecutorService().submit(callable);
- }
-
- public void runOnUiThread(Runnable runnable) {
- App.getMainHandler().post(runnable);
- }
-
- public Future runOnUiThread(Callable callable) {
- var task = new FutureTask<>(callable);
- runOnUiThread(task);
- return task;
- }
-
- public void showHint(@StringRes int res, boolean lengthShort, @StringRes int actionRes, View.OnClickListener action) {
- showHint(App.getInstance().getString(res), lengthShort, App.getInstance().getString(actionRes), action);
- }
-
- public void showHint(@StringRes int res, boolean lengthShort) {
- showHint(App.getInstance().getString(res), lengthShort, null, null);
- }
-
- public void showHint(CharSequence str, boolean lengthShort) {
- showHint(str, lengthShort, null, null);
- }
-
- public void showHint(CharSequence str, boolean lengthShort, CharSequence actionStr, View.OnClickListener action) {
- var container = getView();
- if (isResumed() && container != null) {
- var snackbar = Snackbar.make(container, str, lengthShort ? Snackbar.LENGTH_SHORT : Snackbar.LENGTH_LONG);
- var fab = container.findViewById(R.id.fab);
- if (fab instanceof FloatingActionButton && ((FloatingActionButton) fab).isOrWillBeShown())
- snackbar.setAnchorView(fab);
- if (actionStr != null && action != null) snackbar.setAction(actionStr, action);
- snackbar.show();
- return;
- }
- runOnUiThread(() -> {
- try {
- Toast.makeText(App.getInstance(), str, lengthShort ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
- } catch (Throwable ignored) {
- }
- });
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/CompileDialogFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/CompileDialogFragment.java
deleted file mode 100644
index 9f1b11959..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/CompileDialogFragment.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatDialogFragment;
-import androidx.fragment.app.FragmentManager;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.FragmentCompileDialogBinding;
-import org.lsposed.manager.receivers.LSPManagerServiceHolder;
-import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
-
-import java.lang.ref.WeakReference;
-
-@SuppressWarnings("deprecation")
-public class CompileDialogFragment extends AppCompatDialogFragment {
- public static void speed(FragmentManager fragmentManager, ApplicationInfo info) {
- CompileDialogFragment fragment = new CompileDialogFragment();
- fragment.setCancelable(false);
- var bundle = new Bundle();
- bundle.putParcelable("appInfo", info);
- fragment.setArguments(bundle);
- fragment.show(fragmentManager, "compile_dialog");
- }
-
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- var arguments = getArguments();
- ApplicationInfo appInfo = arguments != null ? arguments.getParcelable("appInfo") : null;
- if (appInfo == null) {
- throw new IllegalStateException("appInfo should not be null.");
- }
-
- FragmentCompileDialogBinding binding = FragmentCompileDialogBinding.inflate(LayoutInflater.from(requireActivity()), null, false);
- final PackageManager pm = requireContext().getPackageManager();
- var builder = new BlurBehindDialogBuilder(requireActivity())
- .setIcon(appInfo.loadIcon(pm))
- .setTitle(appInfo.loadLabel(pm))
- .setView(binding.getRoot());
-
- var alertDialog = builder.create();
- new CompileTask(this).executeOnExecutor(App.getExecutorService(), appInfo.packageName);
- return alertDialog;
- }
-
- private static class CompileTask extends AsyncTask {
-
- WeakReference outerRef;
-
- CompileTask(CompileDialogFragment fragment) {
- outerRef = new WeakReference<>(fragment);
- }
-
- @Override
- protected Throwable doInBackground(String... commands) {
- try {
- if (LSPManagerServiceHolder.getService().optimizePackage(commands[0])) {
- return null;
- } else {
- return new UnknownError();
- }
- } catch (Throwable e) {
- return e;
- }
- }
-
- @Override
- protected void onPostExecute(Throwable result) {
- Context context = App.getInstance();
- String text;
- if (result != null) {
- if (result instanceof UnknownError) {
- text = context.getString(R.string.compile_failed);
- } else {
- text = context.getString(R.string.compile_failed_with_info) + result;
- }
- } else {
- text = context.getString(R.string.compile_done);
- }
- try {
- CompileDialogFragment fragment = outerRef.get();
- if (fragment != null) {
- fragment.dismissAllowingStateLoss();
- var parent = fragment.getParentFragment();
- if (parent instanceof BaseFragment) {
- ((BaseFragment) parent).showHint(text, true);
- }
- }
- } catch (IllegalStateException ignored) {
- }
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java
deleted file mode 100644
index 4036587d1..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.os.Build;
-import android.os.Bundle;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.system.OsConstants;
-import android.text.method.LinkMovementMethod;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.text.HtmlCompat;
-import androidx.core.view.MenuProvider;
-import androidx.fragment.app.DialogFragment;
-
-import org.lsposed.lspd.ILSPManagerService;
-import org.lsposed.manager.BuildConfig;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.DialogAboutBinding;
-import org.lsposed.manager.databinding.FragmentHomeBinding;
-import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
-import org.lsposed.manager.ui.dialog.WelcomeDialog;
-import org.lsposed.manager.util.NavUtil;
-import org.lsposed.manager.util.UpdateUtil;
-import org.lsposed.manager.util.chrome.LinkTransformationMethod;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import rikka.core.util.ClipboardUtils;
-import rikka.material.app.LocaleDelegate;
-
-public class HomeFragment extends BaseFragment implements MenuProvider {
- private FragmentHomeBinding binding;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- WelcomeDialog.showIfNeed(getChildFragmentManager());
- }
-
- @Override
- public void onPrepareMenu(Menu menu) {
- menu.findItem(R.id.menu_about).setOnMenuItemClickListener(v -> {
- showAbout();
- return true;
- });
- menu.findItem(R.id.menu_issue).setOnMenuItemClickListener(v -> {
- NavUtil.startURL(requireActivity(), "https://github.com/JingMatrix/LSPosed/issues/new/choose");
- return true;
- });
- }
-
- @Override
- public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
-
- }
-
- @Override
- public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
- return false;
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- binding = FragmentHomeBinding.inflate(inflater, container, false);
- setupToolbar(binding.toolbar, binding.clickView, R.string.app_name, R.menu.menu_home);
- binding.toolbar.setNavigationIcon(null);
- binding.toolbar.setOnClickListener(v -> showAbout());
- binding.clickView.setOnClickListener(v -> showAbout());
- binding.appBar.setLiftable(true);
- binding.nestedScrollView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top));
-
- updateStates(requireActivity(), ConfigManager.isBinderAlive(), UpdateUtil.needUpdate());
-
- return binding.getRoot();
- }
-
- private void updateStates(Activity activity, boolean binderAlive, boolean needUpdate) {
- if (binderAlive) {
- if (needUpdate) {
- binding.updateTitle.setText(R.string.need_update);
- binding.updateSummary.setText(getString(R.string.please_update_summary));
- binding.statusIcon.setImageResource(R.drawable.ic_round_update_24);
- binding.updateBtn.setOnClickListener(v -> {
- NavUtil.startURL(activity, getString(R.string.latest_url));
- });
- binding.updateCard.setVisibility(View.VISIBLE);
- } else {
- binding.updateCard.setVisibility(View.GONE);
- }
- boolean dex2oatAbnormal = ConfigManager.getDex2OatWrapperCompatibility() != ILSPManagerService.DEX2OAT_OK && !ConfigManager.dex2oatFlagsLoaded();
- var sepolicyAbnormal = !ConfigManager.isSepolicyLoaded();
- var systemServerAbnormal = !ConfigManager.systemServerRequested();
- if (sepolicyAbnormal || systemServerAbnormal || dex2oatAbnormal) {
- binding.statusTitle.setText(R.string.partial_activated);
- binding.statusIcon.setImageResource(R.drawable.ic_round_warning_24);
- binding.warningCard.setVisibility(View.VISIBLE);
- if (sepolicyAbnormal) {
- binding.warningTitle.setText(R.string.selinux_policy_not_loaded_summary);
- binding.warningSummary.setText(HtmlCompat.fromHtml(getString(R.string.selinux_policy_not_loaded), HtmlCompat.FROM_HTML_MODE_LEGACY));
- }
- if (systemServerAbnormal) {
- binding.warningTitle.setText(R.string.system_inject_fail_summary);
- binding.warningSummary.setText(HtmlCompat.fromHtml(getString(R.string.system_inject_fail), HtmlCompat.FROM_HTML_MODE_LEGACY));
- }
- if (dex2oatAbnormal) {
- binding.warningTitle.setText(R.string.system_prop_incorrect_summary);
- binding.warningSummary.setText(HtmlCompat.fromHtml(getString(R.string.system_prop_incorrect), HtmlCompat.FROM_HTML_MODE_LEGACY));
- }
- } else {
- binding.warningCard.setVisibility(View.GONE);
- binding.statusTitle.setText(R.string.activated);
- binding.statusIcon.setImageResource(R.drawable.ic_round_check_circle_24);
- }
- binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)",
- ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode()));
- binding.developerWarningCard.setVisibility(isDeveloper() ? View.VISIBLE : View.GONE);
- } else {
- boolean isMagiskInstalled = ConfigManager.isMagiskInstalled();
- if (isMagiskInstalled) {
- binding.updateTitle.setText(R.string.install);
- binding.updateSummary.setText(R.string.install_summary);
- binding.statusIcon.setImageResource(R.drawable.ic_round_error_outline_24);
- binding.updateBtn.setOnClickListener(v -> {
- NavUtil.startURL(activity, getString(R.string.install_url));
- });
- binding.updateCard.setVisibility(View.VISIBLE);
- } else {
- binding.updateCard.setVisibility(View.GONE);
- }
- binding.warningCard.setVisibility(View.GONE);
- binding.statusTitle.setText(R.string.not_installed);
- binding.statusSummary.setText(R.string.not_install_summary);
- }
-
- if (ConfigManager.isBinderAlive()) {
- binding.apiVersion.setText(String.valueOf(ConfigManager.getXposedApiVersion()));
- binding.frameworkVersion.setText(String.format(LocaleDelegate.getDefaultLocale(), "%1$s (%2$d)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode()));
- binding.managerPackageName.setText(activity.getPackageName());
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.android_version_unsatisfied)));
- } else switch (ConfigManager.getDex2OatWrapperCompatibility()) {
- case ILSPManagerService.DEX2OAT_OK ->
- binding.dex2oatWrapper.setText(R.string.supported);
- case ILSPManagerService.DEX2OAT_CRASHED ->
- binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.crashed)));
- case ILSPManagerService.DEX2OAT_MOUNT_FAILED ->
- binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.mount_failed)));
- case ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE ->
- binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.selinux_permissive)));
- case ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT ->
- binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.sepolicy_incorrect)));
- }
- } else {
- binding.apiVersion.setText(R.string.not_installed);
- binding.frameworkVersion.setText(R.string.not_installed);
- binding.managerPackageName.setText(activity.getPackageName());
- }
-
- if (Build.VERSION.PREVIEW_SDK_INT != 0) {
- binding.systemVersion.setText(String.format(LocaleDelegate.getDefaultLocale(), "%1$s Preview (API %2$d)", Build.VERSION.CODENAME, Build.VERSION.SDK_INT));
- } else {
- binding.systemVersion.setText(String.format(LocaleDelegate.getDefaultLocale(), "%1$s (API %2$d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
- }
-
- binding.device.setText(getDevice());
- binding.systemAbi.setText(Build.SUPPORTED_ABIS[0]);
- String info = activity.getString(R.string.info_api_version) +
- "\n" +
- binding.apiVersion.getText() +
- "\n\n" +
- activity.getString(R.string.info_dex2oat_wrapper) +
- "\n" +
- binding.dex2oatWrapper.getText() +
- "\n\n" +
- activity.getString(R.string.info_framework_version) +
- "\n" +
- binding.frameworkVersion.getText() +
- "\n\n" +
- activity.getString(R.string.info_manager_package_name) +
- "\n" +
- binding.managerPackageName.getText() +
- "\n\n" +
- activity.getString(R.string.info_system_version) +
- "\n" +
- binding.systemVersion.getText() +
- "\n\n" +
- activity.getString(R.string.info_device) +
- "\n" +
- binding.device.getText() +
- "\n\n" +
- activity.getString(R.string.info_system_abi) +
- "\n" +
- binding.systemAbi.getText();
- var map = new HashMap();
- map.put("apiVersion", binding.apiVersion.getText().toString());
- map.put("frameworkVersion", binding.frameworkVersion.getText().toString());
- map.put("systemAbi", Arrays.toString(Build.SUPPORTED_ABIS));
- binding.copyInfo.setOnClickListener(v -> {
- ClipboardUtils.put(activity, info);
- showHint(R.string.info_copied, false);
- });
- }
-
- private String getDevice() {
- String manufacturer = Character.toUpperCase(Build.MANUFACTURER.charAt(0)) + Build.MANUFACTURER.substring(1);
- if (!Build.BRAND.equals(Build.MANUFACTURER)) {
- manufacturer += " " + Character.toUpperCase(Build.BRAND.charAt(0)) + Build.BRAND.substring(1);
- }
- manufacturer += " " + Build.MODEL + " ";
- return manufacturer;
- }
-
- private boolean isDeveloper() {
- var developer = new AtomicBoolean(false);
- var pids = Paths.get("/data/local/tmp/.studio/ipids");
- try (var dir = Files.list(pids)) {
- dir.findFirst().ifPresent(name -> {
- var pid = Integer.parseInt(name.getFileName().toString());
- try {
- Os.kill(pid, 0);
- developer.set(true);
- } catch (ErrnoException e) {
- if (e.errno == OsConstants.ESRCH) {
- try {
- Files.delete(name);
- } catch (IOException ignored) {
- }
- } else {
- developer.set(true);
- }
- }
- });
- } catch (IOException e) {
- return false;
- }
- return developer.get();
- }
-
- public static class AboutDialog extends DialogFragment {
- @NonNull
- @Override
- public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
- DialogAboutBinding binding = DialogAboutBinding.inflate(getLayoutInflater(), null, false);
- binding.designAboutTitle.setText(R.string.app_name);
- binding.designAboutInfo.setMovementMethod(LinkMovementMethod.getInstance());
- binding.designAboutInfo.setTransformationMethod(new LinkTransformationMethod(requireActivity()));
- binding.designAboutInfo.setText(HtmlCompat.fromHtml(getString(
- R.string.about_view_source_code,
- "GitHub ",
- "Telegram "), HtmlCompat.FROM_HTML_MODE_LEGACY));
- binding.designAboutVersion.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
- return new BlurBehindDialogBuilder(requireContext())
- .setView(binding.getRoot()).create();
- }
- }
-
- private void showAbout() {
- new AboutDialog().show(getChildFragmentManager(), "about");
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- binding = null;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/LogsFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/LogsFragment.java
deleted file mode 100644
index 04a46d1d1..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/LogsFragment.java
+++ /dev/null
@@ -1,445 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.annotation.SuppressLint;
-import android.content.ActivityNotFoundException;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.HorizontalScrollView;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.view.MenuProvider;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-
-import com.google.android.material.tabs.TabLayout;
-import com.google.android.material.tabs.TabLayoutMediator;
-import com.google.android.material.textview.MaterialTextView;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.FragmentPagerBinding;
-import org.lsposed.manager.databinding.ItemLogTextviewBinding;
-import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding;
-import org.lsposed.manager.receivers.LSPManagerServiceHolder;
-import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
-import org.lsposed.manager.util.AccessibilityUtils;
-
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.InputStreamReader;
-import java.time.LocalDateTime;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-import rikka.material.app.LocaleDelegate;
-import rikka.recyclerview.RecyclerViewKt;
-
-public class LogsFragment extends BaseFragment implements MenuProvider {
- private FragmentPagerBinding binding;
- private LogPageAdapter adapter;
- private MenuItem wordWrap;
-
- interface OptionsItemSelectListener {
- boolean onOptionsItemSelected(@NonNull MenuItem item);
- }
-
- private OptionsItemSelectListener optionsItemSelectListener;
-
- private final ActivityResultLauncher saveLogsLauncher = registerForActivityResult(
- new ActivityResultContracts.CreateDocument("application/zip"),
- uri -> {
- if (uri == null) return;
- runAsync(() -> {
- var context = requireContext();
- var cr = context.getContentResolver();
- try (var zipFd = cr.openFileDescriptor(uri, "wt")) {
- showHint(context.getString(R.string.logs_saving), false);
- LSPManagerServiceHolder.getService().getLogs(zipFd);
- showHint(context.getString(R.string.logs_saved), true);
- } catch (Throwable e) {
- var cause = e.getCause();
- var message = cause == null ? e.getMessage() : cause.getMessage();
- var text = context.getString(R.string.logs_save_failed2, message);
- showHint(text, false);
- Log.w(App.TAG, "save log", e);
- }
- });
- });
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = FragmentPagerBinding.inflate(inflater, container, false);
- binding.appBar.setLiftable(true);
- setupToolbar(binding.toolbar, binding.clickView, R.string.Logs, R.menu.menu_logs);
- binding.toolbar.setNavigationIcon(null);
- binding.toolbar.setSubtitle(ConfigManager.isVerboseLogEnabled() ? R.string.enabled_verbose_log : R.string.disabled_verbose_log);
- adapter = new LogPageAdapter(this);
- binding.viewPager.setAdapter(adapter);
-
- var isAnimationEnabled = AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver());
- new TabLayoutMediator(
- binding.tabLayout,
- binding.viewPager,
- // `autoRefresh = true` by default. Update the tabs automatically when the data set of the view pager's
- // adapter changes.
- true,
- isAnimationEnabled,
- (tab, position) -> tab.setText((int) adapter.getItemId(position))
- ).attach();
-
- binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0);
- int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum();
- if (tabLayoutWidth <= binding.getRoot().getWidth()) {
- binding.tabLayout.setTabMode(TabLayout.MODE_FIXED);
- binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
- }
- });
-
- return binding.getRoot();
- }
-
- public void setOptionsItemSelectListener(OptionsItemSelectListener optionsItemSelectListener) {
- this.optionsItemSelectListener = optionsItemSelectListener;
- }
-
- @Override
- public boolean onMenuItemSelected(@NonNull MenuItem item) {
- var itemId = item.getItemId();
- if (itemId == R.id.menu_save) {
- save();
- return true;
- } else if (itemId == R.id.menu_word_wrap) {
- item.setChecked(!item.isChecked());
- App.getPreferences().edit().putBoolean("enable_word_wrap", item.isChecked()).apply();
- binding.viewPager.setUserInputEnabled(item.isChecked());
- adapter.refresh();
- return true;
- }
- if (optionsItemSelectListener != null) {
- return optionsItemSelectListener.onOptionsItemSelected(item);
- }
- return false;
- }
-
- @Override
- public void onPrepareMenu(@NonNull Menu menu) {
- wordWrap = menu.findItem(R.id.menu_word_wrap);
- wordWrap.setChecked(App.getPreferences().getBoolean("enable_word_wrap", false));
- binding.viewPager.setUserInputEnabled(wordWrap.isChecked());
- }
-
- @Override
- public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
-
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- binding = null;
- }
-
- private void save() {
- LocalDateTime now = LocalDateTime.now();
- String filename = String.format(LocaleDelegate.getDefaultLocale(), "LSPosed_%s.zip", now.toString());
- try {
- saveLogsLauncher.launch(filename);
- } catch (ActivityNotFoundException e) {
- showHint(R.string.enable_documentui, true);
- }
- }
-
- public static class LogFragment extends BaseFragment {
- public static final int SCROLL_THRESHOLD = 500;
- protected boolean verbose;
- protected SwiperefreshRecyclerviewBinding binding;
- protected LogAdaptor adaptor;
- protected LinearLayoutManager layoutManager;
-
- class LogAdaptor extends EmptyStateRecyclerView.EmptyStateAdapter {
- private List log = Collections.emptyList();
- private boolean isLoaded = false;
-
- @NonNull
- @Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new ViewHolder(ItemLogTextviewBinding.inflate(getLayoutInflater(), parent, false));
- }
-
- @Override
- public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
- holder.item.setText(log.get(position));
- }
-
- @Override
- public int getItemCount() {
- return log.size();
- }
-
- @SuppressLint("NotifyDataSetChanged")
- void refresh(List log) {
- runOnUiThread(() -> {
- isLoaded = true;
- this.log = log;
- notifyDataSetChanged();
- });
- }
-
- void fullRefresh() {
- runAsync(() -> {
- isLoaded = false;
- List tmp;
- try (var parcelFileDescriptor = ConfigManager.getLog(verbose);
- var br = new BufferedReader(new InputStreamReader(new FileInputStream(parcelFileDescriptor != null ? parcelFileDescriptor.getFileDescriptor() : null)))) {
- tmp = br.lines().parallel().collect(Collectors.toList());
- } catch (Throwable e) {
- tmp = Arrays.asList(Log.getStackTraceString(e).split("\n"));
- }
- refresh(tmp);
- });
- }
-
- @Override
- public boolean isLoaded() {
- return isLoaded;
- }
-
- class ViewHolder extends RecyclerView.ViewHolder {
- final MaterialTextView item;
-
- public ViewHolder(ItemLogTextviewBinding binding) {
- super(binding.getRoot());
- item = binding.logItem;
- }
- }
- }
-
- protected LogAdaptor createAdaptor() {
- return new LogAdaptor();
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
- var arguments = getArguments();
- if (arguments == null) return null;
- verbose = arguments.getBoolean("verbose");
- adaptor = createAdaptor();
- binding.recyclerView.setAdapter(adaptor);
- layoutManager = new LinearLayoutManager(requireActivity());
- binding.recyclerView.setLayoutManager(layoutManager);
- // ltr even for rtl languages because of log format
- binding.recyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
- binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
- RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
- binding.swipeRefreshLayout.setOnRefreshListener(adaptor::fullRefresh);
- adaptor.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
- @Override
- public void onChanged() {
- binding.swipeRefreshLayout.setRefreshing(!adaptor.isLoaded());
- }
- });
- adaptor.fullRefresh();
- return binding.getRoot();
- }
-
- public void scrollToTop(LogsFragment logsFragment) {
- logsFragment.binding.appBar.setExpanded(true, true);
- if (layoutManager.findFirstVisibleItemPosition() > SCROLL_THRESHOLD) {
- binding.recyclerView.scrollToPosition(0);
- } else {
- binding.recyclerView.smoothScrollToPosition(0);
- }
- }
-
- public void scrollToBottom(LogsFragment logsFragment) {
- logsFragment.binding.appBar.setExpanded(false, true);
- var end = Math.max(adaptor.getItemCount() - 1, 0);
- if (adaptor.getItemCount() - layoutManager.findLastVisibleItemPosition() > SCROLL_THRESHOLD) {
- binding.recyclerView.scrollToPosition(end);
- } else {
- binding.recyclerView.smoothScrollToPosition(end);
- }
- }
-
- void attachListeners() {
- var parent = getParentFragment();
- if (parent instanceof LogsFragment logsFragment) {
- logsFragment.binding.appBar.setLifted(!binding.recyclerView.getBorderViewDelegate().isShowingTopBorder());
- binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> logsFragment.binding.appBar.setLifted(!top));
- logsFragment.setOptionsItemSelectListener(item -> {
- int itemId = item.getItemId();
- if (itemId == R.id.menu_scroll_top) {
- scrollToTop(logsFragment);
- } else if (itemId == R.id.menu_scroll_down) {
- scrollToBottom(logsFragment);
- } else if (itemId == R.id.menu_clear) {
- if (ConfigManager.clearLogs(verbose)) {
- logsFragment.showHint(R.string.logs_cleared, true);
- adaptor.fullRefresh();
- } else {
- logsFragment.showHint(R.string.logs_clear_failed_2, true);
- }
- return true;
- }
- return false;
- });
-
- View.OnClickListener l = v -> scrollToTop(logsFragment);
- logsFragment.binding.clickView.setOnClickListener(l);
- logsFragment.binding.toolbar.setOnClickListener(l);
- }
- }
-
- void detachListeners() {
- binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener(null);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- attachListeners();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- attachListeners();
- }
-
-
- @Override
- public void onPause() {
- super.onPause();
- detachListeners();
- }
-
- @Override
- public void onStop() {
- super.onStop();
- detachListeners();
- }
- }
-
- public static class UnwrapLogFragment extends LogFragment {
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- var root = super.onCreateView(inflater, container, savedInstanceState);
- binding.swipeRefreshLayout.removeView(binding.recyclerView);
- HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
- horizontalScrollView.setFillViewport(true);
- horizontalScrollView.setHorizontalScrollBarEnabled(false);
- horizontalScrollView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
- if (!AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver())) {
- horizontalScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
- }
- binding.swipeRefreshLayout.addView(horizontalScrollView);
- horizontalScrollView.addView(binding.recyclerView);
- binding.recyclerView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
- return root;
- }
-
- @Override
- protected LogAdaptor createAdaptor() {
- return new LogAdaptor() {
- @Override
- public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
- super.onBindViewHolder(holder, position);
- var view = holder.item;
- view.measure(0, 0);
- int desiredWidth = view.getMeasuredWidth();
- ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
- layoutParams.width = desiredWidth;
- if (binding.recyclerView.getWidth() < desiredWidth) {
- binding.recyclerView.requestLayout();
- }
- }
- };
- }
- }
-
- class LogPageAdapter extends FragmentStateAdapter {
-
- public LogPageAdapter(@NonNull Fragment fragment) {
- super(fragment);
- }
-
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- var bundle = new Bundle();
- bundle.putBoolean("verbose", verbose(position));
- var f = getItemViewType(position) == 0 ? new LogFragment() : new UnwrapLogFragment();
- f.setArguments(bundle);
- return f;
- }
-
- @Override
- public int getItemCount() {
- return 2;
- }
-
- @Override
- public long getItemId(int position) {
- return verbose(position) ? R.string.nav_item_logs_verbose : R.string.nav_item_logs_module;
- }
-
- @Override
- public boolean containsItem(long itemId) {
- return itemId == R.string.nav_item_logs_verbose || itemId == R.string.nav_item_logs_module;
- }
-
- public boolean verbose(int position) {
- return position != 0;
- }
-
- @Override
- public int getItemViewType(int position) {
- return wordWrap.isChecked() ? 0 : 1;
- }
-
- public void refresh() {
- runOnUiThread(this::notifyDataSetChanged);
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/ModulesFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/ModulesFragment.java
deleted file mode 100644
index a44afb6b3..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/ModulesFragment.java
+++ /dev/null
@@ -1,878 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
-
-import android.annotation.SuppressLint;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.Spannable;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.RelativeSizeSpan;
-import android.text.style.StyleSpan;
-import android.text.style.TypefaceSpan;
-import android.util.SparseArray;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Filter;
-import android.widget.Filterable;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.SearchView;
-import androidx.appcompat.widget.TooltipCompat;
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import androidx.core.view.MenuProvider;
-import androidx.fragment.app.Fragment;
-import androidx.navigation.NavOptions;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-import androidx.viewpager2.widget.ViewPager2;
-
-import com.bumptech.glide.request.target.CustomTarget;
-import com.bumptech.glide.request.transition.Transition;
-import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
-import com.google.android.material.checkbox.MaterialCheckBox;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.android.material.tabs.TabLayout;
-import com.google.android.material.tabs.TabLayoutMediator;
-
-import org.lsposed.lspd.models.UserInfo;
-import org.lsposed.manager.App;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-import org.lsposed.manager.adapters.AppHelper;
-import org.lsposed.manager.databinding.FragmentPagerBinding;
-import org.lsposed.manager.databinding.ItemModuleBinding;
-import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding;
-import org.lsposed.manager.repo.RepoLoader;
-import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
-import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
-import org.lsposed.manager.util.GlideApp;
-import org.lsposed.manager.util.ModuleUtil;
-
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.stream.IntStream;
-
-import rikka.core.util.ResourceUtils;
-import rikka.material.app.LocaleDelegate;
-import rikka.recyclerview.RecyclerViewKt;
-
-public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleListener, RepoLoader.RepoListener, MenuProvider {
- private static final PackageManager pm = App.getInstance().getPackageManager();
- private static final ModuleUtil moduleUtil = ModuleUtil.getInstance();
- private static final RepoLoader repoLoader = RepoLoader.getInstance();
- protected FragmentPagerBinding binding;
- protected SearchView searchView;
- private SearchView.OnQueryTextListener searchListener;
-
- SparseArray adapters = new SparseArray<>();
- PagerAdapter pagerAdapter = null;
-
- private ModuleUtil.InstalledModule selectedModule;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- searchListener = new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- forEachAdaptor(adapter -> adapter.getFilter().filter(query));
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String query) {
- forEachAdaptor(adapter -> adapter.getFilter().filter(query));
- return false;
- }
- };
- }
-
- private void forEachAdaptor(Consumer super ModuleAdapter> action) {
- var snapshot = adapters;
- for (var i = 0; i < snapshot.size(); ++i) {
- action.accept(snapshot.valueAt(i));
- }
- }
-
- private void showFab() {
- var layoutParams = binding.fab.getLayoutParams();
- if (layoutParams instanceof CoordinatorLayout.LayoutParams) {
- var coordinatorLayoutBehavior =
- ((CoordinatorLayout.LayoutParams) layoutParams).getBehavior();
- if (coordinatorLayoutBehavior instanceof HideBottomViewOnScrollBehavior) {
- //noinspection unchecked
- ((HideBottomViewOnScrollBehavior) coordinatorLayoutBehavior).slideUp(binding.fab);
- }
- }
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = FragmentPagerBinding.inflate(inflater, container, false);
- binding.appBar.setLiftable(true);
- setupToolbar(binding.toolbar, binding.clickView, R.string.Modules, R.menu.menu_modules);
- binding.toolbar.setNavigationIcon(null);
- pagerAdapter = new PagerAdapter(this);
- binding.viewPager.setAdapter(pagerAdapter);
- binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
- @Override
- public void onPageSelected(int position) {
- showFab();
- }
- });
-
- new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> {
- if (position < adapters.size()) {
- tab.setText(adapters.valueAt(position).getUser().name);
- }
- }).attach();
-
- binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0);
- int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum();
- if (tabLayoutWidth <= binding.getRoot().getWidth()) {
- binding.tabLayout.setTabMode(TabLayout.MODE_FIXED);
- binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
- }
- });
-
- binding.fab.setOnClickListener(v -> {
- var bundle = new Bundle();
- var user = adapters.valueAt(binding.viewPager.getCurrentItem()).getUser();
- bundle.putParcelable("userInfo", user);
- var f = new RecyclerViewDialogFragment();
- f.setArguments(bundle);
- f.show(getChildFragmentManager(), "install_to_user" + user.id);
- });
-
- moduleUtil.addListener(this);
- repoLoader.addListener(this);
- onModulesReloaded();
-
- return binding.getRoot();
- }
-
- @Override
- public void onPrepareMenu(Menu menu) {
- searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
- if (searchView != null) {
- searchView.setOnQueryTextListener(searchListener);
- searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(@NonNull View arg0) {
- binding.appBar.setExpanded(false, true);
- }
-
- @Override
- public void onViewDetachedFromWindow(@NonNull View v) {
- }
- });
- searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
- }
- }
-
- @Override
- public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
-
- }
-
- @Override
- public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
- return false;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- forEachAdaptor(ModuleAdapter::refresh);
- }
-
- @Override
- public void onSingleModuleReloaded(ModuleUtil.InstalledModule module) {
- forEachAdaptor(ModuleAdapter::refresh);
- }
-
- @Override
- public void onModulesReloaded() {
- var users = moduleUtil.getUsers();
- if (users == null) return;
-
- if (users.size() != 1) {
- binding.viewPager.setUserInputEnabled(true);
- binding.tabLayout.setVisibility(View.VISIBLE);
- binding.fab.show();
- } else {
- binding.viewPager.setUserInputEnabled(false);
- binding.tabLayout.setVisibility(View.GONE);
- }
-
- var tmp = new SparseArray(users.size());
- var snapshot = adapters;
- for (var user : users) {
- if (snapshot.indexOfKey(user.id) >= 0) {
- tmp.put(user.id, snapshot.get(user.id));
- } else {
- var adapter = new ModuleAdapter(user);
- adapter.setHasStableIds(true);
- tmp.put(user.id, adapter);
- }
- }
- adapters = tmp;
- forEachAdaptor(ModuleAdapter::refresh);
- runOnUiThread(pagerAdapter::notifyDataSetChanged);
- updateModuleSummary();
- }
-
- @Override
- public void onRepoLoaded() {
- forEachAdaptor(ModuleAdapter::refresh);
- }
-
- private void updateModuleSummary() {
- var moduleCount = moduleUtil.getEnabledModulesCount();
- runOnUiThread(() -> {
- if (binding != null) {
- binding.toolbar.setSubtitle(moduleCount == -1 ? getString(R.string.loading) : getResources().getQuantityString(R.plurals.modules_enabled_count, moduleCount, moduleCount));
- binding.toolbarLayout.setSubtitle(binding.toolbar.getSubtitle());
- }
- });
- }
-
- void installModuleToUser(ModuleUtil.InstalledModule module, UserInfo user) {
- new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons)
- .setTitle(getString(R.string.install_to_user, user.name))
- .setMessage(getString(R.string.install_to_user_message, module.getAppName(), user.name))
- .setPositiveButton(android.R.string.ok, (dialog, which) ->
- runAsync(() -> {
- var success = ConfigManager.installExistingPackageAsUser(module.packageName, user.id);
- String text = success ?
- getString(R.string.module_installed, module.getAppName(), user.name) :
- getString(R.string.module_install_failed);
- showHint(text, false);
- if (success)
- moduleUtil.reloadSingleModule(module.packageName, user.id);
- }))
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
-
- @SuppressLint("WrongConstant")
- @Override
- public boolean onContextItemSelected(@NonNull MenuItem item) {
- if (selectedModule == null) {
- return false;
- }
- int itemId = item.getItemId();
- if (itemId == R.id.menu_launch) {
- String packageName = selectedModule.packageName;
- if (packageName == null) {
- return false;
- }
- Intent intent = AppHelper.getSettingsIntent(packageName, selectedModule.userId);
- if (intent != null) {
- ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId);
- }
- return true;
- } else if (itemId == R.id.menu_other_app) {
- var intent = new Intent(Intent.ACTION_SHOW_APP_INFO);
- intent.putExtra(Intent.EXTRA_PACKAGE_NAME, selectedModule.packageName);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId);
- return true;
- } else if (itemId == R.id.menu_app_info) {
- ConfigManager.startActivityAsUserWithFeature(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", selectedModule.packageName, null)), selectedModule.userId);
- return true;
- } else if (itemId == R.id.menu_uninstall) {
- new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_FullWidthButtons)
- .setIcon(selectedModule.app.loadIcon(pm))
- .setTitle(selectedModule.getAppName())
- .setMessage(R.string.module_uninstall_message)
- .setPositiveButton(android.R.string.ok, (dialog, which) ->
- runAsync(() -> {
- boolean success = ConfigManager.uninstallPackage(selectedModule.packageName, selectedModule.userId);
- String text = success ? getString(R.string.module_uninstalled, selectedModule.getAppName()) : getString(R.string.module_uninstall_failed);
- showHint(text, false);
- if (success)
- moduleUtil.reloadSingleModule(selectedModule.packageName, selectedModule.userId);
- }))
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- return true;
- } else if (itemId == R.id.menu_repo) {
- var navController = getNavController();
- navController.navigate(
- new Uri.Builder().scheme("lsposed").authority("repo").appendQueryParameter("modulePackageName", selectedModule.packageName).build(),
- new NavOptions.Builder().setEnterAnim(R.anim.fragment_enter).setExitAnim(R.anim.fragment_exit).setPopEnterAnim(R.anim.fragment_enter_pop).setPopExitAnim(R.anim.fragment_exit_pop).setLaunchSingleTop(true).setPopUpTo(getNavController().getGraph().getStartDestinationId(), false, true).build());
- return true;
- } else if (itemId == R.id.menu_compile_speed) {
- CompileDialogFragment.speed(getChildFragmentManager(), selectedModule.pkg.applicationInfo);
- }
- return super.onContextItemSelected(item);
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- moduleUtil.removeListener(this);
- repoLoader.removeListener(this);
- binding = null;
- }
-
- public static class ModuleListFragment extends Fragment {
- public SwiperefreshRecyclerviewBinding binding;
- private ModuleAdapter adapter;
- private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
- @Override
- public void onChanged() {
- binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded());
- }
- };
-
- private final View.OnAttachStateChangeListener searchViewLocker = new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(@NonNull View v) {
- binding.recyclerView.setNestedScrollingEnabled(false);
- }
-
- @Override
- public void onViewDetachedFromWindow(@NonNull View v) {
- binding.recyclerView.setNestedScrollingEnabled(true);
- }
- };
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- ModulesFragment fragment = (ModulesFragment) getParentFragment();
- Bundle arguments = getArguments();
- if (fragment == null || arguments == null) {
- return null;
- }
- int userId = arguments.getInt("user_id");
- binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
- adapter = fragment.adapters.get(userId);
- binding.recyclerView.setAdapter(adapter);
- binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
- binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh);
- binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
- RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
- adapter.registerAdapterDataObserver(observer);
- return binding.getRoot();
- }
-
- void attachListeners() {
- var parent = getParentFragment();
- if (parent instanceof ModulesFragment moduleFragment) {
- binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> moduleFragment.binding.appBar.setLifted(!top));
- moduleFragment.binding.appBar.setLifted(!binding.recyclerView.getBorderViewDelegate().isShowingTopBorder());
- moduleFragment.searchView.addOnAttachStateChangeListener(searchViewLocker);
- binding.recyclerView.setNestedScrollingEnabled(moduleFragment.searchView.isIconified());
- View.OnClickListener l = v -> {
- if (moduleFragment.searchView.isIconified()) {
- binding.recyclerView.smoothScrollToPosition(0);
- moduleFragment.binding.appBar.setExpanded(true, true);
- }
- };
- moduleFragment.binding.clickView.setOnClickListener(l);
- moduleFragment.binding.toolbar.setOnClickListener(l);
- }
- }
-
- void detachListeners() {
- binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener(null);
- var parent = getParentFragment();
- if (parent instanceof ModulesFragment moduleFragment) {
- moduleFragment.searchView.removeOnAttachStateChangeListener(searchViewLocker);
- binding.recyclerView.setNestedScrollingEnabled(true);
- }
- }
-
- @Override
- public void onStart() {
- super.onStart();
- attachListeners();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- attachListeners();
- }
-
- @Override
- public void onDestroyView() {
- adapter.unregisterAdapterDataObserver(observer);
- super.onDestroyView();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- detachListeners();
- }
-
- @Override
- public void onStop() {
- super.onStop();
- detachListeners();
- }
- }
-
- private class PagerAdapter extends FragmentStateAdapter {
-
- public PagerAdapter(@NonNull Fragment fragment) {
- super(fragment);
- }
-
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- Bundle bundle = new Bundle();
- bundle.putInt("user_id", adapters.keyAt(position));
- Fragment fragment = new ModuleListFragment();
- fragment.setArguments(bundle);
- return fragment;
- }
-
- @Override
- public int getItemCount() {
- return adapters.size();
- }
-
- @Override
- public long getItemId(int position) {
- return adapters.keyAt(position);
- }
-
- @Override
- public boolean containsItem(long itemId) {
- return adapters.indexOfKey((int) itemId) >= 0;
- }
- }
-
- ModuleAdapter createPickModuleAdapter(UserInfo userInfo) {
- return new ModuleAdapter(userInfo, true);
- }
-
- class ModuleAdapter extends EmptyStateRecyclerView.EmptyStateAdapter implements Filterable {
- private List searchList = new ArrayList<>();
- private List showList = new ArrayList<>();
- private final UserInfo user;
- private final boolean isPick;
- private boolean isLoaded;
- private View.OnClickListener onPickListener;
-
- ModuleAdapter(UserInfo user) {
- this(user, false);
- }
-
- ModuleAdapter(UserInfo user, boolean isPick) {
- this.user = user;
- this.isPick = isPick;
- }
-
- public UserInfo getUser() {
- return user;
- }
-
- @NonNull
- @Override
- public ModuleAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new ViewHolder(ItemModuleBinding.inflate(getLayoutInflater(), parent, false));
- }
-
- public boolean isPick() {
- return isPick;
- }
-
- @Override
- public void onBindViewHolder(@NonNull ModuleAdapter.ViewHolder holder, int position) {
- ModuleUtil.InstalledModule item = showList.get(position);
- String appName;
- if (item.userId != 0) {
- appName = String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", item.getAppName(), item.userId);
- } else {
- appName = item.getAppName();
- }
- holder.appName.setText(appName);
- GlideApp.with(holder.appIcon)
- .load(item.getPackageInfo())
- .into(new CustomTarget() {
- @Override
- public void onResourceReady(@NonNull Drawable resource, @Nullable Transition super Drawable> transition) {
- holder.appIcon.setImageDrawable(resource);
- }
-
- @Override
- public void onLoadCleared(@Nullable Drawable placeholder) {
-
- }
- });
- SpannableStringBuilder sb = new SpannableStringBuilder();
- if (!item.getDescription().isEmpty()) {
- sb.append(item.getDescription());
- } else {
- sb.append(getString(R.string.module_empty_description));
- }
- holder.appDescription.setText(sb);
- holder.appDescription.setVisibility(View.VISIBLE);
- sb = new SpannableStringBuilder();
-
- int installXposedVersion = ConfigManager.getXposedApiVersion();
- bindApiBadge(holder.apiBadge, item, installXposedVersion);
- String warningText = null;
- if (item.minVersion == 0) {
- warningText = getString(R.string.no_min_version_specified);
- } else if (installXposedVersion > 0 && item.minVersion > installXposedVersion) {
- warningText = getString(R.string.warning_xposed_min_version, item.minVersion);
- } else if (item.targetVersion > installXposedVersion) {
- warningText = getString(R.string.warning_target_version_higher, item.targetVersion);
- } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) {
- warningText = getString(R.string.warning_min_version_too_low, item.minVersion, ModuleUtil.MIN_MODULE_VERSION);
- } else if (item.isInstalledOnExternalStorage()) {
- warningText = getString(R.string.warning_installed_on_external_storage);
- }
- if (warningText != null) {
- sb.append(warningText);
- final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorError));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
- sb.setSpan(typefaceSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- } else {
- final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
- sb.setSpan(styleSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
- sb.setSpan(foregroundColorSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
- var ver = repoLoader.getModuleLatestVersion(item.packageName);
- if (ver != null && ver.upgradable(item.versionCode, item.versionName)) {
- if (warningText != null) sb.append("\n");
- String recommended = getString(R.string.update_available, ver.versionName);
- sb.append(recommended);
- final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), androidx.appcompat.R.attr.colorPrimary));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
- sb.setSpan(typefaceSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- } else {
- final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
- sb.setSpan(styleSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
- sb.setSpan(foregroundColorSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
- if (sb.length() == 0) {
- holder.hint.setVisibility(View.GONE);
- } else {
- holder.hint.setVisibility(View.VISIBLE);
- holder.hint.setText(sb);
- }
-
- if (!isPick) {
- holder.root.setAlpha(moduleUtil.isModuleEnabled(item.packageName) ? 1.0f : .5f);
- holder.itemView.setOnClickListener(v -> {
- searchView.clearFocus();
- if (isLoaded()) {
- safeNavigate(ModulesFragmentDirections.actionModulesFragmentToAppListFragment(item.packageName, item.userId));
- }
- });
- holder.itemView.setOnLongClickListener(v -> {
- searchView.clearFocus();
- selectedModule = item;
- return false;
- });
- holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
- requireActivity().getMenuInflater().inflate(R.menu.context_menu_modules, menu);
- menu.setHeaderTitle(item.getAppName());
- Intent intent = AppHelper.getSettingsIntent(item.packageName, item.userId);
- if (intent == null) {
- menu.removeItem(R.id.menu_launch);
- }
- if (repoLoader.getOnlineModule(item.packageName) == null) {
- menu.removeItem(R.id.menu_repo);
- }
- if (item.userId == 0) {
- var users = ConfigManager.getUsers();
- if (users != null) {
- for (var user : users) {
- if (moduleUtil.getModule(item.packageName, user.id) == null) {
- menu.add(1, user.id, 0, getString(R.string.install_to_user, user.name)).setOnMenuItemClickListener(i -> {
- installModuleToUser(selectedModule, user);
- return true;
- });
- }
- }
- }
- }
- });
- holder.appVersion.setVisibility(View.VISIBLE);
- holder.appVersion.setText(item.versionName);
- holder.appVersion.setSelected(true);
- } else {
- holder.itemView.setTag(item);
- holder.itemView.setOnClickListener(v -> {
- if (onPickListener != null) onPickListener.onClick(v);
- });
- }
- }
-
- @Override
- public void onViewRecycled(@NonNull ViewHolder holder) {
- holder.itemView.setTag(null);
- super.onViewRecycled(holder);
- }
-
- @Override
- public int getItemCount() {
- return showList.size();
- }
-
- @Override
- public long getItemId(int position) {
- var module = showList.get(position);
- return (module.packageName + "!" + module.userId).hashCode();
- }
-
- @Override
- public Filter getFilter() {
- return new ModuleAdapter.ApplicationFilter();
- }
-
- public void setOnPickListener(View.OnClickListener onPickListener) {
- this.onPickListener = onPickListener;
- }
-
- public void refresh() {
- runAsync(reloadModules);
- }
-
- public void fullRefresh() {
- runAsync(() -> {
- setLoaded(null, false);
- moduleUtil.reloadInstalledModules();
- refresh();
- });
- }
-
- private final Runnable reloadModules = () -> {
- var modules = moduleUtil.getModules();
- if (modules == null) return;
- Comparator cmp = AppHelper.getAppListComparator(0, pm);
- setLoaded(null, false);
- var tmpList = new ArrayList();
- modules.values().parallelStream()
- .sorted((a, b) -> {
- boolean aChecked = moduleUtil.isModuleEnabled(a.packageName);
- boolean bChecked = moduleUtil.isModuleEnabled(b.packageName);
- if (aChecked == bChecked) {
- var c = cmp.compare(a.pkg, b.pkg);
- if (c == 0) {
- if (a.userId == getUser().id) return -1;
- if (b.userId == getUser().id) return 1;
- else return Integer.compare(a.userId, b.userId);
- }
- return c;
- } else if (aChecked) {
- return -1;
- } else {
- return 1;
- }
- }).forEachOrdered(new Consumer<>() {
- private final HashSet uniquer = new HashSet<>();
-
- @Override
- public void accept(ModuleUtil.InstalledModule module) {
- if (isPick()) {
- if (!uniquer.contains(module.packageName)) {
- uniquer.add(module.packageName);
- if (module.userId != getUser().id)
- tmpList.add(module);
- }
- } else if (module.userId == getUser().id) {
- tmpList.add(module);
- }
- }
- });
- String queryStr = searchView != null ? searchView.getQuery().toString() : "";
- searchList = tmpList;
- runOnUiThread(() -> getFilter().filter(queryStr));
- };
-
- @SuppressLint("NotifyDataSetChanged")
- private void setLoaded(List list, boolean loaded) {
- runOnUiThread(() -> {
- if (list != null) showList = list;
- isLoaded = loaded;
- notifyDataSetChanged();
- });
- }
-
- @Override
- public boolean isLoaded() {
- return isLoaded && moduleUtil.isModulesLoaded();
- }
-
- /**
- * The API version a module targets, printed under its icon. The word stays in the ordinary
- * secondary colour and the version itself carries the colour, so the eye lands on the one
- * part that differs between modules. The whole statement goes in the content description
- * and the tooltip, so the colour is never the only thing carrying it.
- */
- private void bindApiBadge(TextView badge, ModuleUtil.InstalledModule item, int frameworkApi) {
- final String label;
- final String status;
- final String version;
- final int color;
-
- if (item.legacy) {
- // A legacy module reports the original Xposed API, which is numbered separately
- // from the one this framework implements, so it is never compared against it.
- version = item.minVersion > 0 ? String.valueOf(item.minVersion) : null;
- label = version != null
- ? getString(R.string.module_api_badge_legacy, item.minVersion)
- : getString(R.string.module_api_badge_legacy_unknown);
- status = getString(R.string.module_api_status_legacy);
- color = android.R.attr.textColorSecondary;
- } else if (item.minVersion <= 0 || item.targetVersion <= 0 || frameworkApi <= 0) {
- // Both keys are required of a module. Reporting the one it did declare would put a
- // version on screen for a module that never stated which API it was built for.
- version = null;
- label = getString(R.string.module_api_badge_unknown);
- status = getString(R.string.module_api_status_unknown);
- color = android.R.attr.textColorSecondary;
- } else {
- version = String.valueOf(item.targetVersion);
- label = getString(R.string.module_api_badge, item.targetVersion);
- if (item.minVersion > frameworkApi) {
- // It declares it needs more than we have, whatever it targets.
- status = getString(R.string.module_api_status_unsupported, item.minVersion, frameworkApi);
- color = com.google.android.material.R.attr.colorError;
- } else if (item.targetVersion == frameworkApi) {
- status = getString(R.string.module_api_status_current);
- color = androidx.appcompat.R.attr.colorPrimary;
- } else {
- status = getString(item.targetVersion > frameworkApi
- ? R.string.module_api_status_newer
- : R.string.module_api_status_older,
- item.targetVersion, frameworkApi);
- color = android.R.attr.textColorSecondary;
- }
- }
-
- final int resolved = ResourceUtils.resolveColor(requireActivity().getTheme(), color);
- final SpannableStringBuilder text = new SpannableStringBuilder(label);
- final int at = version == null ? -1 : label.lastIndexOf(version);
- if (at > 0) {
- // Small caps: the capitals of the word set smaller than the version beside them, so
- // the word reads as a label and the version reads as the value.
- text.setSpan(new RelativeSizeSpan(0.78f), 0, at, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
- text.setSpan(new StyleSpan(Typeface.BOLD), at, label.length(),
- Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- text.setSpan(new ForegroundColorSpan(resolved), at, label.length(),
- Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- } else {
- text.setSpan(new RelativeSizeSpan(0.78f), 0, label.length(),
- Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- text.setSpan(new ForegroundColorSpan(resolved), 0, label.length(),
- Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
-
- badge.setText(text);
- badge.setContentDescription(status);
- TooltipCompat.setTooltipText(badge, status);
- badge.setVisibility(View.VISIBLE);
- }
-
- static class ViewHolder extends RecyclerView.ViewHolder {
- ConstraintLayout root;
- ImageView appIcon;
- TextView appName;
- TextView appDescription;
- TextView appVersion;
- TextView apiBadge;
- TextView hint;
- MaterialCheckBox checkBox;
-
- ViewHolder(ItemModuleBinding binding) {
- super(binding.getRoot());
- root = binding.itemRoot;
- appIcon = binding.appIcon;
- appName = binding.appName;
- appDescription = binding.description;
- appVersion = binding.versionName;
- apiBadge = binding.apiBadge;
- hint = binding.hint;
- checkBox = binding.checkbox;
- }
- }
-
- class ApplicationFilter extends Filter {
-
- private boolean lowercaseContains(String s, String filter) {
- return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
- }
-
- @Override
- protected FilterResults performFiltering(CharSequence constraint) {
- FilterResults filterResults = new FilterResults();
- List filtered = new ArrayList<>();
- String filter = constraint.toString().toLowerCase();
- for (ModuleUtil.InstalledModule info : searchList) {
- if (lowercaseContains(info.getAppName(), filter) ||
- lowercaseContains(info.packageName, filter) ||
- lowercaseContains(info.getDescription(), filter)) {
- filtered.add(info);
- }
- }
- filterResults.values = filtered;
- filterResults.count = filtered.size();
- return filterResults;
- }
-
- @Override
- protected void publishResults(CharSequence constraint, FilterResults results) {
- //noinspection unchecked
- setLoaded((List) results.values, true);
- }
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RecyclerViewDialogFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RecyclerViewDialogFragment.java
deleted file mode 100644
index 5a49c8d74..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/RecyclerViewDialogFragment.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.app.Dialog;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatDialogFragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.lsposed.lspd.models.UserInfo;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.DialogTitleBinding;
-import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding;
-import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
-import org.lsposed.manager.util.ModuleUtil;
-
-public class RecyclerViewDialogFragment extends AppCompatDialogFragment {
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- var parent = getParentFragment();
- var arguments = getArguments();
- if (!(parent instanceof ModulesFragment) || arguments == null) {
- throw new IllegalStateException();
- }
- var modulesFragment = (ModulesFragment) parent;
- var user = (UserInfo) arguments.getParcelable("userInfo");
-
- var pickAdaptor = modulesFragment.createPickModuleAdapter(user);
- var binding = SwiperefreshRecyclerviewBinding.inflate(LayoutInflater.from(requireActivity()), null, false);
-
- binding.recyclerView.setAdapter(pickAdaptor);
- binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
- pickAdaptor.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
- @Override
- public void onChanged() {
- binding.swipeRefreshLayout.setRefreshing(!pickAdaptor.isLoaded());
- }
- });
- binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
- binding.swipeRefreshLayout.setOnRefreshListener(pickAdaptor::fullRefresh);
- pickAdaptor.refresh();
- var title = DialogTitleBinding.inflate(getLayoutInflater()).getRoot();
- title.setText(getString(R.string.install_to_user, user.name));
- var dialog = new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_FullWidthButtons)
- .setCustomTitle(title)
- .setView(binding.getRoot())
- .setNegativeButton(android.R.string.cancel, null)
- .create();
- title.setOnClickListener(s -> binding.recyclerView.smoothScrollToPosition(0));
- pickAdaptor.setOnPickListener(picked -> {
- var module = (ModuleUtil.InstalledModule) picked.getTag();
- modulesFragment.installModuleToUser(module, user);
- dialog.dismiss();
- });
- onViewCreated(binding.getRoot(), savedInstanceState);
- return dialog;
- }
-
- // prevent from overriding
- public final void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoFragment.java
deleted file mode 100644
index d1a65b32e..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoFragment.java
+++ /dev/null
@@ -1,470 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.annotation.SuppressLint;
-import android.content.res.Resources;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.text.Spannable;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
-import android.text.style.TypefaceSpan;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.webkit.WebView;
-import android.widget.Filter;
-import android.widget.Filterable;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.SearchView;
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.core.view.MenuProvider;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.FragmentRepoBinding;
-import org.lsposed.manager.databinding.ItemOnlinemoduleBinding;
-import org.lsposed.manager.repo.RepoLoader;
-import org.lsposed.manager.repo.model.OnlineModule;
-import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
-import org.lsposed.manager.util.ModuleUtil;
-
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.time.format.FormatStyle;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-
-import rikka.core.util.LabelComparator;
-import rikka.core.util.ResourceUtils;
-import rikka.recyclerview.RecyclerViewKt;
-
-public class RepoFragment extends BaseFragment implements RepoLoader.RepoListener, ModuleUtil.ModuleListener, MenuProvider {
- protected FragmentRepoBinding binding;
- protected SearchView searchView;
- private SearchView.OnQueryTextListener mSearchListener;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
- private boolean preLoadWebview = true;
-
- private final RepoLoader repoLoader = RepoLoader.getInstance();
- private final ModuleUtil moduleUtil = ModuleUtil.getInstance();
- private RepoAdapter adapter;
- private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
- @Override
- public void onChanged() {
- binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded());
- }
- };
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- mSearchListener = new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- adapter.getFilter().filter(query);
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String newText) {
- adapter.getFilter().filter(newText);
- return false;
- }
- };
- super.onCreate(savedInstanceState);
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = FragmentRepoBinding.inflate(getLayoutInflater(), container, false);
- binding.appBar.setLiftable(true);
- binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top));
- setupToolbar(binding.toolbar, binding.clickView, R.string.module_repo, R.menu.menu_repo);
- binding.toolbar.setNavigationIcon(null);
- adapter = new RepoAdapter();
- adapter.setHasStableIds(true);
- adapter.registerAdapterDataObserver(observer);
- binding.recyclerView.setAdapter(adapter);
- binding.recyclerView.setHasFixedSize(true);
- binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
- RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
- binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh);
- binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
- View.OnClickListener l = v -> {
- if (searchView.isIconified()) {
- binding.recyclerView.smoothScrollToPosition(0);
- binding.appBar.setExpanded(true, true);
- }
- };
- binding.toolbar.setOnClickListener(l);
- binding.clickView.setOnClickListener(l);
- repoLoader.addListener(this);
- moduleUtil.addListener(this);
- onRepoLoaded();
- return binding.getRoot();
- }
-
- private void updateRepoSummary() {
- final int[] count = new int[]{0};
- HashSet processedModules = new HashSet<>();
- var modules = moduleUtil.getModules();
- if (modules != null && repoLoader.isRepoLoaded()) {
- modules.forEach((k, v) -> {
- if (!processedModules.contains(k.first)) {
- var ver = repoLoader.getModuleLatestVersion(k.first);
- if (ver != null && ver.upgradable(v.versionCode, v.versionName)) {
- ++count[0];
- }
- processedModules.add(k.first);
- }
- }
- );
- } else {
- count[0] = -1;
- }
- runOnUiThread(() -> {
- if (binding != null) {
- if (count[0] > 0) {
- binding.toolbar.setSubtitle(getResources().getQuantityString(R.plurals.module_repo_upgradable, count[0], count[0]));
- } else if (count[0] == 0) {
- binding.toolbar.setSubtitle(getResources().getString(R.string.module_repo_up_to_date));
- } else {
- binding.toolbar.setSubtitle(getResources().getString(R.string.loading));
- }
- binding.toolbarLayout.setSubtitle(binding.toolbar.getSubtitle());
- }
- });
- }
-
- @Override
- public void onPrepareMenu(Menu menu) {
- searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
- if (searchView != null) {
- searchView.setOnQueryTextListener(mSearchListener);
- searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(@NonNull View arg0) {
- binding.appBar.setExpanded(false, true);
- binding.recyclerView.setNestedScrollingEnabled(false);
- }
-
- @Override
- public void onViewDetachedFromWindow(@NonNull View v) {
- binding.recyclerView.setNestedScrollingEnabled(true);
- }
- });
- searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
- }
- int sort = App.getPreferences().getInt("repo_sort", 0);
- if (sort == 0) {
- menu.findItem(R.id.item_sort_by_name).setChecked(true);
- } else if (sort == 1) {
- menu.findItem(R.id.item_sort_by_update_time).setChecked(true);
- }
- menu.findItem(R.id.item_upgradable_first).setChecked(App.getPreferences().getBoolean("upgradable_first", true));
- }
-
- @Override
- public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- mHandler.removeCallbacksAndMessages(null);
- repoLoader.removeListener(this);
- moduleUtil.removeListener(this);
- adapter.unregisterAdapterDataObserver(observer);
- binding = null;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- adapter.refresh();
- if (preLoadWebview) {
- mHandler.postDelayed(() -> new WebView(requireContext()), 500);
- preLoadWebview = false;
- }
- }
-
- @Override
- public void onRepoLoaded() {
- if (adapter != null) {
- adapter.refresh();
- }
- updateRepoSummary();
- }
-
- @Override
- public void onThrowable(Throwable t) {
- showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true);
- updateRepoSummary();
- }
-
- @Override
- public void onModulesReloaded() {
- updateRepoSummary();
- }
-
- @Override
- public boolean onMenuItemSelected(@NonNull MenuItem item) {
- int itemId = item.getItemId();
- if (itemId == R.id.item_sort_by_name) {
- item.setChecked(true);
- App.getPreferences().edit().putInt("repo_sort", 0).apply();
- adapter.refresh();
- } else if (itemId == R.id.item_sort_by_update_time) {
- item.setChecked(true);
- App.getPreferences().edit().putInt("repo_sort", 1).apply();
- adapter.refresh();
- } else if (itemId == R.id.item_upgradable_first) {
- item.setChecked(!item.isChecked());
- App.getPreferences().edit().putBoolean("upgradable_first", item.isChecked()).apply();
- adapter.refresh();
- } else {
- return false;
- }
- return true;
- }
-
- private class RepoAdapter extends EmptyStateRecyclerView.EmptyStateAdapter implements Filterable {
- private List fullList, showList;
- private final LabelComparator labelComparator = new LabelComparator();
- private boolean isLoaded = false;
- private final Resources resources = App.getInstance().getResources();
- private final String[] channels = resources.getStringArray(R.array.update_channel_values);
- private String channel;
- private final RepoLoader repoLoader = RepoLoader.getInstance();
-
- RepoAdapter() {
- fullList = showList = Collections.emptyList();
- }
-
- @NonNull
- @Override
- public RepoAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new ViewHolder(ItemOnlinemoduleBinding.inflate(getLayoutInflater(), parent, false));
- }
-
- RepoLoader.ModuleVersion getUpgradableVer(OnlineModule module) {
- ModuleUtil.InstalledModule installedModule = moduleUtil.getModule(module.getName());
- if (installedModule != null) {
- var ver = repoLoader.getModuleLatestVersion(installedModule.packageName);
- if (ver != null && ver.upgradable(installedModule.versionCode, installedModule.versionName))
- return ver;
- }
- return null;
- }
-
- @Override
- public void onBindViewHolder(@NonNull RepoAdapter.ViewHolder holder, int position) {
- OnlineModule module = showList.get(position);
- holder.appName.setText(module.getDescription());
- holder.appPackageName.setText(module.getName());
- Instant instant;
- channel = App.getPreferences().getString("update_channel", channels[0]);
- var latestReleaseTime = repoLoader.getLatestReleaseTime(module.getName(), channel);
- instant = Instant.parse(latestReleaseTime != null ? latestReleaseTime : module.getLatestReleaseTime());
- var formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
- .withLocale(App.getLocale()).withZone(ZoneId.systemDefault());
- holder.publishedTime.setText(String.format(getString(R.string.module_repo_updated_time), formatter.format(instant)));
- SpannableStringBuilder sb = new SpannableStringBuilder();
-
- String summary = module.getSummary();
- if (summary != null) {
- sb.append(summary);
- }
- holder.appDescription.setVisibility(View.VISIBLE);
- holder.appDescription.setText(sb);
- sb = new SpannableStringBuilder();
- var upgradableVer = getUpgradableVer(module);
- if (upgradableVer != null) {
- String hint = getString(R.string.update_available, upgradableVer.versionName);
- sb.append(hint);
- final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorPrimary));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
- sb.setSpan(typefaceSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- } else {
- final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
- sb.setSpan(styleSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
- sb.setSpan(foregroundColorSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- } else if (moduleUtil.getModule(module.getName()) != null) {
- String installed = getString(R.string.installed);
- sb.append(installed);
- final StyleSpan styleSpan = new StyleSpan(Typeface.ITALIC);
- sb.setSpan(styleSpan, sb.length() - installed.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorSecondary));
- sb.setSpan(foregroundColorSpan, sb.length() - installed.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
- }
- if (sb.length() > 0) {
- holder.hint.setVisibility(View.VISIBLE);
- holder.hint.setText(sb);
- } else {
- holder.hint.setVisibility(View.GONE);
- }
-
- holder.itemView.setOnClickListener(v -> {
- searchView.clearFocus();
- safeNavigate(RepoFragmentDirections.actionRepoFragmentToRepoItemFragment(module.getName()));
- });
- holder.itemView.setTooltipText(module.getDescription());
- }
-
- @Override
- public int getItemCount() {
- return showList.size();
- }
-
- @SuppressLint("NotifyDataSetChanged")
- private void setLoaded(List list, boolean isLoaded) {
- runOnUiThread(() -> {
- if (list != null) showList = list;
- this.isLoaded = isLoaded;
- notifyDataSetChanged();
- });
- }
-
- public void setData(Collection modules) {
- if (modules == null) return;
- setLoaded(null, false);
- channel = App.getPreferences().getString("update_channel", channels[0]);
- int sort = App.getPreferences().getInt("repo_sort", 0);
- boolean upgradableFirst = App.getPreferences().getBoolean("upgradable_first", true);
- ConcurrentHashMap upgradable = new ConcurrentHashMap<>();
- fullList = modules.parallelStream().filter((onlineModule -> !onlineModule.isHide() && !(repoLoader.getReleases(onlineModule.getName()) != null && repoLoader.getReleases(onlineModule.getName()).isEmpty())))
- .sorted((a, b) -> {
- if (upgradableFirst) {
- var aUpgrade = upgradable.computeIfAbsent(a.getName(), n -> getUpgradableVer(a) != null);
- var bUpgrade = upgradable.computeIfAbsent(b.getName(), n -> getUpgradableVer(b) != null);
- if (aUpgrade && !bUpgrade) return -1;
- else if (!aUpgrade && bUpgrade) return 1;
- }
- if (sort == 0) {
- return labelComparator.compare(a.getDescription(), b.getDescription());
- } else {
- return Instant.parse(repoLoader.getLatestReleaseTime(b.getName(), channel)).compareTo(Instant.parse(repoLoader.getLatestReleaseTime(a.getName(), channel)));
- }
- }).collect(Collectors.toList());
- String queryStr = searchView != null ? searchView.getQuery().toString() : "";
- runOnUiThread(() -> getFilter().filter(queryStr));
- }
-
- public void fullRefresh() {
- runAsync(() -> {
- setLoaded(null, false);
- repoLoader.loadRemoteData();
- refresh();
- });
- }
-
- public void refresh() {
- runAsync(() -> adapter.setData(repoLoader.getOnlineModules()));
- }
-
- @Override
- public long getItemId(int position) {
- return showList.get(position).getName().hashCode();
- }
-
- @Override
- public Filter getFilter() {
- return new RepoAdapter.ModuleFilter();
- }
-
- @Override
- public boolean isLoaded() {
- return isLoaded && repoLoader.isRepoLoaded();
- }
-
- static class ViewHolder extends RecyclerView.ViewHolder {
- ConstraintLayout root;
- TextView appName;
- TextView appPackageName;
- TextView appDescription;
- TextView hint;
- TextView publishedTime;
-
- ViewHolder(ItemOnlinemoduleBinding binding) {
- super(binding.getRoot());
- root = binding.itemRoot;
- appName = binding.appName;
- appPackageName = binding.appPackageName;
- appDescription = binding.description;
- hint = binding.hint;
- publishedTime = binding.publishedTime;
- }
- }
-
- class ModuleFilter extends Filter {
-
- private boolean lowercaseContains(String s, String filter) {
- return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
- }
-
- @Override
- protected FilterResults performFiltering(CharSequence constraint) {
- FilterResults filterResults = new FilterResults();
- ArrayList filtered = new ArrayList<>();
- String filter = constraint.toString().toLowerCase();
- for (OnlineModule info : fullList) {
- if (lowercaseContains(info.getDescription(), filter) ||
- lowercaseContains(info.getName(), filter) ||
- lowercaseContains(info.getSummary(), filter)) {
- filtered.add(info);
- }
- }
- filterResults.values = filtered;
- filterResults.count = filtered.size();
- return filterResults;
- }
-
- @Override
- protected void publishResults(CharSequence constraint, FilterResults results) {
- //noinspection unchecked
- setLoaded((List) results.values, true);
- }
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java
deleted file mode 100644
index 4f64c8f48..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java
+++ /dev/null
@@ -1,824 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.text.Spannable;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.format.Formatter;
-import android.text.style.ClickableSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.RelativeSizeSpan;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.webkit.WebResourceRequest;
-import android.webkit.WebResourceResponse;
-import android.webkit.WebSettings;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.widget.ArrayAdapter;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.view.MenuProvider;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-
-import com.google.android.material.button.MaterialButton;
-import com.google.android.material.progressindicator.CircularProgressIndicator;
-import com.google.android.material.tabs.TabLayout;
-import com.google.android.material.tabs.TabLayoutMediator;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.FragmentPagerBinding;
-import org.lsposed.manager.databinding.ItemRepoLoadmoreBinding;
-import org.lsposed.manager.databinding.ItemRepoReadmeBinding;
-import org.lsposed.manager.databinding.ItemRepoRecyclerviewBinding;
-import org.lsposed.manager.databinding.ItemRepoReleaseBinding;
-import org.lsposed.manager.databinding.ItemRepoTitleDescriptionBinding;
-import org.lsposed.manager.repo.RepoLoader;
-import org.lsposed.manager.repo.model.Collaborator;
-import org.lsposed.manager.repo.model.OnlineModule;
-import org.lsposed.manager.repo.model.Release;
-import org.lsposed.manager.repo.model.ReleaseAsset;
-import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
-import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
-import org.lsposed.manager.ui.widget.LinkifyTextView;
-import org.lsposed.manager.util.AccessibilityUtils;
-import org.lsposed.manager.util.NavUtil;
-import org.lsposed.manager.util.SimpleStatefulAdaptor;
-import org.lsposed.manager.util.chrome.CustomTabsURLSpan;
-
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.time.format.FormatStyle;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-import okhttp3.Headers;
-import okhttp3.Request;
-import okhttp3.Response;
-import rikka.core.util.ResourceUtils;
-import rikka.material.app.LocaleDelegate;
-import rikka.recyclerview.RecyclerViewKt;
-import rikka.widget.borderview.BorderView;
-
-public class RepoItemFragment extends BaseFragment implements RepoLoader.RepoListener, MenuProvider {
- FragmentPagerBinding binding;
- OnlineModule module;
- private ReleaseAdapter releaseAdapter;
- private InformationAdapter informationAdapter;
- private boolean remoteModuleLoadRequested = false;
- private boolean releaseLoadRequestedByUser = false;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = FragmentPagerBinding.inflate(getLayoutInflater(), container, false);
- if (module == null) return binding.getRoot();
- String modulePackageName = module.getName();
- String moduleName = module.getDescription();
- binding.appBar.setLiftable(true);
- setupToolbar(binding.toolbar, binding.clickView, moduleName, R.menu.menu_repo_item);
- binding.clickView.setTooltipText(moduleName);
- binding.toolbar.setSubtitle(modulePackageName);
- binding.viewPager.setAdapter(new PagerAdapter(this));
- int[] titles = new int[]{R.string.module_readme, R.string.module_releases, R.string.module_information};
-
- var isAnimationEnabled = AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver());
- new TabLayoutMediator(
- binding.tabLayout,
- binding.viewPager,
- // `autoRefresh = true` by default. Update the tabs automatically when the data set of the view pager's
- // adapter changes.
- true,
- isAnimationEnabled,
- (tab, position) -> tab.setText(titles[position])
- ).attach();
-
- binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0);
- int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum();
- if (tabLayoutWidth <= binding.getRoot().getWidth()) {
- binding.tabLayout.setTabMode(TabLayout.MODE_FIXED);
- binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
- }
- });
- binding.toolbar.setOnClickListener(v -> binding.appBar.setExpanded(true, true));
- releaseAdapter = new ReleaseAdapter();
- informationAdapter = new InformationAdapter();
- RepoLoader.getInstance().addListener(this);
- loadRemoteModuleIfReadmeMissing();
- return binding.getRoot();
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- RepoLoader.getInstance().addListener(this);
- super.onCreate(savedInstanceState);
-
- String modulePackageName = getArguments() == null ? null : getArguments().getString("modulePackageName");
- module = RepoLoader.getInstance().getOnlineModule(modulePackageName);
- Log.i(App.TAG, "RepoItem: open " + modulePackageName + " -> module " + (module == null ? "NOT FOUND (repoLoaded=" + RepoLoader.getInstance().isRepoLoaded() + "), navigating back" : "found"));
- if (module == null) {
- if (!safeNavigate(R.id.action_repo_item_fragment_to_repo_fragment)) {
- safeNavigate(R.id.repo_nav);
- }
- }
- }
-
- private void renderGithubMarkdown(WebView view, @Nullable String text) {
- try {
- view.setBackgroundColor(Color.TRANSPARENT);
- var setting = view.getSettings();
- setting.setOffscreenPreRaster(true);
- setting.setDomStorageEnabled(true);
- setting.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
- setting.setAllowContentAccess(false);
- setting.setAllowFileAccessFromFileURLs(true);
- setting.setAllowFileAccess(false);
- setting.setGeolocationEnabled(false);
- setting.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
- setting.setTextZoom(80);
- String body;
- String direction;
- if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
- direction = "rtl";
- } else {
- direction = "ltr";
- }
- if (TextUtils.isEmpty(text)) {
- text = "" + App.getInstance().getString(R.string.list_empty) + " ";
- }
- if (ResourceUtils.isNightMode(getResources().getConfiguration())) {
- body = App.HTML_TEMPLATE_DARK.get().replace("@dir@", direction).replace("@body@", text);
- } else {
- body = App.HTML_TEMPLATE.get().replace("@dir@", direction).replace("@body@", text);
- }
- view.setWebViewClient(new WebViewClient() {
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
- NavUtil.startURL(requireActivity(), request.getUrl());
- return true;
- }
-
- @Nullable
- @Override
- public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
- if (!request.getUrl().getScheme().startsWith("http")) return null;
- var client = App.getOkHttpClient();
- var call = client.newCall(
- new Request.Builder()
- .url(request.getUrl().toString())
- .method(request.getMethod(), null)
- .headers(Headers.of(request.getRequestHeaders()))
- .build());
- try {
- Response reply = call.execute();
- var header = reply.header("content-type", "image/*;charset=utf-8");
- String[] contentTypes = new String[0];
- if (header != null) {
- contentTypes = header.split(";\\s*");
- }
- var mimeType = contentTypes.length > 0 ? contentTypes[0] : "image/*";
- var charset = contentTypes.length > 1 ? contentTypes[1].split("=\\s*")[1] : "utf-8";
- var body = reply.body();
- if (body == null) return null;
- return new WebResourceResponse(
- mimeType,
- charset,
- body.byteStream()
- );
- } catch (Throwable e) {
- return new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream(Log.getStackTraceString(e).getBytes(StandardCharsets.UTF_8)));
- }
- }
- });
- view.loadDataWithBaseURL("https://github.com", body, "text/html",
- StandardCharsets.UTF_8.name(), null);
- } catch (Throwable e) {
- Log.e(App.TAG, "render readme", e);
- }
- }
-
- @Nullable
- private OnlineModule refreshModuleFromRepo() {
- if (module == null || module.getName() == null) return module;
- var updatedModule = RepoLoader.getInstance().getOnlineModule(module.getName());
- if (updatedModule != null) {
- // A repo refresh can replace RepoLoader's entry with the summary
- // object from modules.json, which lacks README/release detail that
- // was already fetched for this fragment. Keep the richer instance so
- // the UI does not flicker back to empty/truncated content.
- var currentHasDetail = module.releasesLoaded || hasReadme(module);
- var updatedHasDetail = updatedModule.releasesLoaded || hasReadme(updatedModule);
- if (!currentHasDetail || updatedHasDetail) {
- module = updatedModule;
- }
- }
- return module;
- }
-
- private boolean hasReadme(@Nullable OnlineModule module) {
- return module != null && (!TextUtils.isEmpty(module.getReadmeHTML()) || !TextUtils.isEmpty(module.getReadme()));
- }
-
- private void loadRemoteModuleIfReadmeMissing() {
- var currentModule = refreshModuleFromRepo();
- if (currentModule == null || currentModule.getName() == null) return;
- if (remoteModuleLoadRequested || currentModule.releasesLoaded || hasReadme(currentModule)) return;
-
- remoteModuleLoadRequested = true;
- RepoLoader.getInstance().loadRemoteReleases(currentModule.getName());
- }
-
- // True while the per-module detail (which carries the README) is still being
- // fetched, so the README tab can show a loading state instead of the empty
- // placeholder on a slow connection.
- private boolean isModuleDetailLoading() {
- return remoteModuleLoadRequested;
- }
-
- @Nullable
- private String getModuleReadme() {
- var currentModule = refreshModuleFromRepo();
- if (currentModule == null) return null;
- String readme = currentModule.getReadmeHTML();
- if (TextUtils.isEmpty(readme)) {
- readme = currentModule.getReadme();
- }
- if (TextUtils.isEmpty(readme)) {
- loadRemoteModuleIfReadmeMissing();
- }
- return readme;
- }
-
- @Override
- public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
-
- }
-
- @Override
- public boolean onMenuItemSelected(@NonNull MenuItem item) {
- int id = item.getItemId();
- if (id == R.id.menu_open_in_browser) {
- NavUtil.startURL(requireActivity(), "https://modules.lsposed.org/module/" + module.getName());
- return true;
- }
- return false;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- RepoLoader.getInstance().removeListener(this);
- remoteModuleLoadRequested = false;
- binding = null;
- }
-
- @Override
- public void onRepoLoaded() {
- refreshModuleFromRepo();
- loadRemoteModuleIfReadmeMissing();
- if (releaseAdapter != null) {
- runAsync(releaseAdapter::loadItems);
- }
- }
-
- @Override
- public void onModuleReleasesLoaded(OnlineModule module) {
- if (this.module == null || module == null || !TextUtils.equals(this.module.getName(), module.getName())) return;
- this.module = module;
- remoteModuleLoadRequested = false;
- var repoLoader = RepoLoader.getInstance();
- if (releaseAdapter != null) {
- runAsync(releaseAdapter::loadItems);
- }
- if (releaseLoadRequestedByUser && (repoLoader.getReleases(module.getName()) != null ? repoLoader.getReleases(module.getName()).size() : 1) == 1) {
- showHint(R.string.module_release_no_more, true);
- }
- releaseLoadRequestedByUser = false;
- }
-
- @Override
- public void onThrowable(Throwable t) {
- remoteModuleLoadRequested = false;
- releaseLoadRequestedByUser = false;
- if (releaseAdapter != null) {
- runAsync(releaseAdapter::loadItems);
- }
- showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true);
- }
-
- private class InformationAdapter extends SimpleStatefulAdaptor {
-
- private int rowCount = 0;
- private int homepageRow = -1;
- private int collaboratorsRow = -1;
- private int sourceUrlRow = -1;
-
- public InformationAdapter() {
- if (!TextUtils.isEmpty(module.getHomepageUrl())) {
- homepageRow = rowCount++;
- }
- if (module.getCollaborators() != null && !module.getCollaborators().isEmpty()) {
- collaboratorsRow = rowCount++;
- }
- if (!TextUtils.isEmpty(module.getSourceUrl())) {
- sourceUrlRow = rowCount++;
- }
- }
-
- @NonNull
- @Override
- public InformationAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- return new ViewHolder(ItemRepoTitleDescriptionBinding.inflate(getLayoutInflater(), parent, false));
- }
-
- @Override
- public void onBindViewHolder(@NonNull InformationAdapter.ViewHolder holder, int position) {
- if (position == homepageRow) {
- holder.title.setText(R.string.module_information_homepage);
- holder.description.setText(module.getHomepageUrl());
- } else if (position == collaboratorsRow) {
- List collaborators = module.getCollaborators();
- if (collaborators == null) return;
- holder.title.setText(R.string.module_information_collaborators);
- SpannableStringBuilder sb = new SpannableStringBuilder();
- ListIterator iterator = collaborators.listIterator();
- while (iterator.hasNext()) {
- Collaborator collaborator = iterator.next();
- var collaboratorLogin = collaborator.getLogin();
- if (collaboratorLogin == null) continue;
- String name = collaborator.getName() == null ? collaboratorLogin : collaborator.getName();
- sb.append(name);
- CustomTabsURLSpan span = new CustomTabsURLSpan(requireActivity(), String.format("https://github.com/%s", collaborator.getLogin()));
- sb.setSpan(span, sb.length() - name.length(), sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- if (iterator.hasNext()) {
- sb.append(", ");
- }
- }
- holder.description.setText(sb);
- } else if (position == sourceUrlRow) {
- holder.title.setText(R.string.module_information_source_url);
- holder.description.setText(module.getSourceUrl());
- }
- holder.itemView.setOnClickListener(v -> {
- if (position == homepageRow) {
- NavUtil.startURL(requireActivity(), module.getHomepageUrl());
- } else if (position == collaboratorsRow) {
- ClickableSpan span = holder.description.getCurrentSpan();
- holder.description.clearCurrentSpan();
-
- if (span instanceof CustomTabsURLSpan) {
- span.onClick(v);
- }
- } else if (position == sourceUrlRow) {
- NavUtil.startURL(requireActivity(), module.getSourceUrl());
- }
- });
- }
-
- @Override
- public int getItemCount() {
- return rowCount;
- }
-
- class ViewHolder extends RecyclerView.ViewHolder {
- TextView title;
- LinkifyTextView description;
-
- public ViewHolder(ItemRepoTitleDescriptionBinding binding) {
- super(binding.getRoot());
- title = binding.title;
- description = binding.description;
- }
- }
- }
-
- public static class DownloadDialog extends DialogFragment {
- @NonNull
- @Override
- public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
- var args = getArguments();
- if (args == null) throw new IllegalArgumentException();
- return new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons)
- .setTitle(R.string.module_release_view_assets)
- .setPositiveButton(android.R.string.cancel, null)
- .setAdapter(new ArrayAdapter<>(requireActivity(), R.layout.dialog_item, args.getCharSequenceArray("names")),
- (dialog, which) -> NavUtil.startURL(requireActivity(), args.getStringArrayList("urls").get(which)))
- .create();
- }
-
- static void create(Activity activity, FragmentManager fm, List assets) {
- var f = new DownloadDialog();
- var bundle = new Bundle();
-
- var displayNames = new CharSequence[assets.size()];
- for (int i = 0; i < assets.size(); i++) {
- var sb = new SpannableStringBuilder(assets.get(i).getName());
- var count = assets.get(i).getDownloadCount();
- var countStr = activity.getResources().getQuantityString(R.plurals.module_release_assets_download_count, count, count);
- var sizeStr = Formatter.formatShortFileSize(activity, assets.get(i).getSize());
- sb.append('\n').append(sizeStr).append('/').append(countStr);
- final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(activity.getTheme(), android.R.attr.textColorSecondary));
- final RelativeSizeSpan relativeSizeSpan = new RelativeSizeSpan(0.8f);
- sb.setSpan(foregroundColorSpan, sb.length() - sizeStr.length() - countStr.length() - 1, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- sb.setSpan(relativeSizeSpan, sb.length() - sizeStr.length() - countStr.length() - 1, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- displayNames[i] = sb;
- }
- bundle.putCharSequenceArray("names", displayNames);
- bundle.putStringArrayList("urls", assets.stream().map(ReleaseAsset::getDownloadUrl).collect(Collectors.toCollection(ArrayList::new)));
- f.setArguments(bundle);
- f.show(fm, "download");
- }
- }
-
- private class ReleaseAdapter extends EmptyStateRecyclerView.EmptyStateAdapter {
- private List items = new ArrayList<>();
- private final Resources resources = App.getInstance().getResources();
-
- public ReleaseAdapter() {
- runAsync(this::loadItems);
- }
-
- @SuppressLint("NotifyDataSetChanged")
- public void loadItems() {
- var channels = resources.getStringArray(R.array.update_channel_values);
- var channel = App.getPreferences().getString("update_channel", channels[0]);
- // Prefer this fragment's module when its releases were already loaded
- // in full; a repo refresh may have replaced RepoLoader's entry with
- // the modules.json summary, whose truncated release list would
- // shadow the complete data we already fetched.
- List releases = module.releasesLoaded ? module.getReleases() : null;
- if (releases == null) releases = RepoLoader.getInstance().getReleases(module.getName());
- if (releases == null) releases = module.getReleases();
- List tmpList;
- if (channel.equals(channels[0])) {
- tmpList = releases != null ? releases.parallelStream().filter(t -> {
- if (Boolean.TRUE.equals(t.getIsPrerelease())) return false;
- var name = t.getName() != null ? t.getName().toLowerCase(LocaleDelegate.getDefaultLocale()) : null;
- return !(name != null && name.startsWith("snapshot")) && !(name != null && name.startsWith("nightly"));
- }).collect(Collectors.toList()) : null;
- } else if (channel.equals(channels[1])) {
- tmpList = releases != null ? releases.parallelStream().filter(t -> {
- var name = t.getName() != null ? t.getName().toLowerCase(LocaleDelegate.getDefaultLocale()) : null;
- return !(name != null && name.startsWith("snapshot")) && !(name != null && name.startsWith("nightly"));
- }).collect(Collectors.toList()) : null;
- } else tmpList = releases;
- List newItems = tmpList != null ? tmpList : new ArrayList<>();
- runOnUiThread(() -> {
- items = newItems;
- notifyDataSetChanged();
- });
- }
-
- @NonNull
- @Override
- public ReleaseAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- if (viewType == 0) {
- return new ReleaseViewHolder(ItemRepoReleaseBinding.inflate(getLayoutInflater(), parent, false));
- } else {
- return new LoadmoreViewHolder(ItemRepoLoadmoreBinding.inflate(getLayoutInflater(), parent, false));
- }
- }
-
- @Override
- public void onBindViewHolder(@NonNull ReleaseAdapter.ViewHolder holder, int position) {
- if (holder.getItemViewType() == 1) {
- holder.progress.setVisibility(View.GONE);
- holder.title.setVisibility(View.VISIBLE);
- holder.itemView.setOnClickListener(v -> {
- if (holder.progress.getVisibility() == View.GONE) {
- holder.title.setVisibility(View.GONE);
- holder.progress.show();
- releaseLoadRequestedByUser = true;
- RepoLoader.getInstance().loadRemoteReleases(module.getName());
- }
- });
- } else {
- Release release = items.get(position);
- holder.title.setText(release.getName());
- var instant = Instant.parse(release.getPublishedAt());
- var formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
- .withLocale(App.getLocale()).withZone(ZoneId.systemDefault());
- holder.publishedTime.setText(String.format(getString(R.string.module_repo_published_time), formatter.format(instant)));
- renderGithubMarkdown(holder.description, release.getDescriptionHTML());
- holder.openInBrowser.setOnClickListener(v -> NavUtil.startURL(requireActivity(), release.getUrl()));
- List assets = release.getReleaseAssets();
- if (assets != null && !assets.isEmpty()) {
- holder.viewAssets.setOnClickListener(v -> DownloadDialog.create(requireActivity(), getParentFragmentManager(), assets));
- } else {
- holder.viewAssets.setVisibility(View.GONE);
- }
- }
- }
-
- @Override
- public int getItemCount() {
- return items.size() + (module.releasesLoaded ? 0 : 1);
- }
-
- @Override
- public int getItemViewType(int position) {
- return !module.releasesLoaded && position == getItemCount() - 1 ? 1 : 0;
- }
-
- @Override
- public boolean isLoaded() {
- return module.releasesLoaded;
- }
-
- class ViewHolder extends RecyclerView.ViewHolder {
- TextView title;
- TextView publishedTime;
- WebView description;
- MaterialButton openInBrowser;
- MaterialButton viewAssets;
- CircularProgressIndicator progress;
-
- public ViewHolder(@NonNull View itemView) {
- super(itemView);
- }
- }
-
- class ReleaseViewHolder extends ReleaseAdapter.ViewHolder {
- public ReleaseViewHolder(ItemRepoReleaseBinding binding) {
- super(binding.getRoot());
- title = binding.title;
- publishedTime = binding.publishedTime;
- description = binding.description;
- openInBrowser = binding.openInBrowser;
- viewAssets = binding.viewAssets;
- }
- }
-
- class LoadmoreViewHolder extends ReleaseAdapter.ViewHolder {
- public LoadmoreViewHolder(ItemRepoLoadmoreBinding binding) {
- super(binding.getRoot());
- title = binding.title;
- progress = binding.progress;
- }
- }
- }
-
- private static class PagerAdapter extends FragmentStateAdapter {
-
- public PagerAdapter(@NonNull Fragment fragment) {
- super(fragment);
- }
-
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- Bundle bundle = new Bundle();
- bundle.putInt("position", position);
- Fragment f;
- if (position == 0) {
- f = new ReadmeFragment();
- } else if (position == 1) {
- f = new RecyclerviewFragment();
- } else {
- f = new RecyclerviewFragment();
- }
- f.setArguments(bundle);
- return f;
- }
-
- @Override
- public int getItemCount() {
- return 3;
- }
-
- @Override
- public int getItemViewType(int position) {
- return position == 0 ? 0 : 1;
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
- }
-
- public static abstract class BorderFragment extends BaseFragment {
- BorderView borderView;
-
- void attachListeners() {
- var parent = getParentFragment();
- if (parent instanceof RepoItemFragment) {
- var repoItemFragment = (RepoItemFragment) parent;
- borderView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> repoItemFragment.binding.appBar.setLifted(!top));
- repoItemFragment.binding.appBar.setLifted(!borderView.getBorderViewDelegate().isShowingTopBorder());
- repoItemFragment.binding.toolbar.setOnClickListener(v -> {
- repoItemFragment.binding.appBar.setExpanded(true, true);
- scrollToTop();
- });
- }
- }
-
- abstract void scrollToTop();
-
- void detachListeners() {
- borderView.getBorderViewDelegate().setBorderVisibilityChangedListener(null);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- attachListeners();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- attachListeners();
- }
-
- @Override
- public void onStop() {
- super.onStop();
- detachListeners();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- detachListeners();
- }
- }
-
- public static class ReadmeFragment extends BorderFragment implements RepoLoader.RepoListener {
- ItemRepoReadmeBinding binding;
- private String renderedReadme;
- private boolean readmeRendered = false;
-
- private void renderReadme() {
- var parent = getParentFragment();
- if (!(parent instanceof RepoItemFragment) || binding == null) return;
-
- var repoItemFragment = (RepoItemFragment) parent;
- // getModuleReadme() also kicks off the per-module fetch when the
- // README is missing, so query the loading state afterwards.
- var readme = repoItemFragment.getModuleReadme();
- String display;
- if (!TextUtils.isEmpty(readme)) {
- display = readme;
- } else if (repoItemFragment.isModuleDetailLoading()) {
- // Detail is still downloading (e.g. slow connection); show a
- // loading placeholder rather than the empty state so users are
- // not misled into thinking the module has no README.
- display = "" + getString(R.string.loading) + " ";
- } else {
- // Detail has loaded and there is genuinely no README; let
- // renderGithubMarkdown fall back to the empty placeholder.
- display = null;
- }
- var pkg = repoItemFragment.module == null ? null : repoItemFragment.module.getName();
- Log.i(App.TAG, "RepoItem: render README for " + pkg + " -> " + (!TextUtils.isEmpty(readme) ? "content" : repoItemFragment.isModuleDetailLoading() ? "loading" : "empty"));
- // onRepoLoaded fires on every repo load and channel change; skip the
- // WebView reload when the rendered content has not actually changed
- // to avoid flicker.
- if (readmeRendered && TextUtils.equals(renderedReadme, display)) return;
- renderedReadme = display;
- readmeRendered = true;
- repoItemFragment.renderGithubMarkdown(binding.readme, display);
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- var parent = getParentFragment();
- if (!(parent instanceof RepoItemFragment)) {
- if (!safeNavigate(R.id.action_repo_item_fragment_to_repo_fragment)) {
- safeNavigate(R.id.repo_nav);
- }
- return null;
- }
- binding = ItemRepoReadmeBinding.inflate(getLayoutInflater(), container, false);
- borderView = binding.scrollView;
- RepoLoader.getInstance().addListener(this);
- renderReadme();
- return binding.getRoot();
- }
-
- @Override
- public void onRepoLoaded() {
- if (binding != null) {
- runOnUiThread(this::renderReadme);
- }
- }
-
- @Override
- public void onModuleReleasesLoaded(OnlineModule module) {
- if (binding != null) {
- var parent = getParentFragment();
- if (parent instanceof RepoItemFragment) {
- var repoItemFragment = (RepoItemFragment) parent;
- if (repoItemFragment.module != null && TextUtils.equals(repoItemFragment.module.getName(), module.getName())) {
- runOnUiThread(this::renderReadme);
- }
- }
- }
- }
-
- @Override
- public void onThrowable(Throwable t) {
- // The fetch failed; re-render so the tab leaves the loading state
- // (the parent already reset the in-flight flag before this runnable
- // executes) instead of spinning forever.
- if (binding != null) {
- runOnUiThread(this::renderReadme);
- }
- }
-
- @Override
- public void onDestroyView() {
- RepoLoader.getInstance().removeListener(this);
- binding = null;
- renderedReadme = null;
- readmeRendered = false;
- super.onDestroyView();
- }
-
- @Override
- void scrollToTop() {
- binding.scrollView.fullScroll(ScrollView.FOCUS_UP);
- }
- }
-
- public static class RecyclerviewFragment extends BorderFragment {
- ItemRepoRecyclerviewBinding binding;
- RecyclerView.Adapter> adapter;
-
- @Override
- void scrollToTop() {
- binding.recyclerView.smoothScrollToPosition(0);
- }
-
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- var arguments = getArguments();
- var parent = getParentFragment();
- if (arguments == null || !(parent instanceof RepoItemFragment)) {
- if (!safeNavigate(R.id.action_repo_item_fragment_to_repo_fragment)) {
- safeNavigate(R.id.repo_nav);
- }
- return null;
- }
- var repoItemFragment = (RepoItemFragment) parent;
- var position = arguments.getInt("position", 0);
- if (position == 1)
- adapter = repoItemFragment.releaseAdapter;
- else if (position == 2)
- adapter = repoItemFragment.informationAdapter;
- else return null;
- binding = ItemRepoRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
- binding.recyclerView.setAdapter(adapter);
- binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
- RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
- borderView = binding.recyclerView;
- return binding.getRoot();
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java
deleted file mode 100644
index 501bcef0a..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.fragment;
-
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatDelegate;
-import androidx.core.text.HtmlCompat;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceFragmentCompat;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.color.DynamicColors;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.BuildConfig;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.R;
-import org.lsposed.manager.databinding.FragmentSettingsBinding;
-import org.lsposed.manager.repo.RepoLoader;
-import org.lsposed.manager.ui.activity.MainActivity;
-import org.lsposed.manager.util.BackupUtils;
-import org.lsposed.manager.util.CloudflareDNS;
-import org.lsposed.manager.util.LangList;
-import org.lsposed.manager.util.NavUtil;
-import org.lsposed.manager.util.ShortcutUtil;
-import org.lsposed.manager.util.ThemeUtil;
-
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Locale;
-
-import rikka.core.util.ResourceUtils;
-import rikka.material.app.LocaleDelegate;
-import rikka.material.preference.MaterialSwitchPreference;
-import rikka.preference.SimpleMenuPreference;
-import rikka.recyclerview.RecyclerViewKt;
-import rikka.widget.borderview.BorderRecyclerView;
-
-public class SettingsFragment extends BaseFragment {
- FragmentSettingsBinding binding;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = FragmentSettingsBinding.inflate(inflater, container, false);
- binding.appBar.setLiftable(true);
- setupToolbar(binding.toolbar, binding.clickView, R.string.Settings);
- binding.toolbar.setNavigationIcon(null);
- if (savedInstanceState == null) {
- getChildFragmentManager().beginTransaction().add(R.id.setting_container, new PreferenceFragment()).commitNow();
- }
- if (ConfigManager.isBinderAlive()) {
- binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode()));
- } else {
- binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getString(R.string.not_installed)));
- }
- return binding.getRoot();
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- binding = null;
- }
-
- public static class PreferenceFragment extends PreferenceFragmentCompat {
- private SettingsFragment parentFragment;
-
- ActivityResultLauncher backupLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("application/gzip"), uri -> {
- if (uri == null || parentFragment == null) return;
- parentFragment.runAsync(() -> {
- try {
- BackupUtils.backup(uri);
- } catch (Exception e) {
- var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage());
- parentFragment.showHint(text, false);
- }
- });
- });
- ActivityResultLauncher restoreLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), uri -> {
- if (uri == null || parentFragment == null) return;
- parentFragment.runAsync(() -> {
- try {
- BackupUtils.restore(uri);
- } catch (Exception e) {
- var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage());
- parentFragment.showHint(text, false);
- }
- });
- });
-
- @Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
-
- parentFragment = (SettingsFragment) requireParentFragment();
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
-
- parentFragment = null;
- }
-
- @Override
- public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
- final String SYSTEM = "SYSTEM";
-
- addPreferencesFromResource(R.xml.prefs);
-
- boolean installed = ConfigManager.isBinderAlive();
- MaterialSwitchPreference prefVerboseLogs = findPreference("disable_verbose_log");
- if (prefVerboseLogs != null) {
- prefVerboseLogs.setEnabled(!BuildConfig.DEBUG && installed);
- if (BuildConfig.DEBUG) ConfigManager.setVerboseLogEnabled(false);
- prefVerboseLogs.setChecked(!installed || !ConfigManager.isVerboseLogEnabled());
- prefVerboseLogs.setOnPreferenceChangeListener((preference, newValue) -> ConfigManager.setVerboseLogEnabled(!(boolean) newValue));
- }
-
- MaterialSwitchPreference notificationPreference = findPreference("enable_status_notification");
- if (notificationPreference != null) {
- notificationPreference.setVisible(installed);
- if (installed) {
- notificationPreference.setChecked(ConfigManager.enableStatusNotification());
- notificationPreference.setSummaryOn(R.string.settings_enable_status_notification_summary);
- notificationPreference.setEnabled(true);
- }
- notificationPreference.setOnPreferenceChangeListener((p, v) ->
- ConfigManager.setEnableStatusNotification((boolean) v)
- );
- }
-
- Preference shortcut = findPreference("add_shortcut");
- if (shortcut != null) {
- shortcut.setVisible(App.isParasitic);
- if (!ShortcutUtil.isRequestPinShortcutSupported(requireContext())) {
- shortcut.setEnabled(false);
- shortcut.setSummary(R.string.settings_unsupported_pin_shortcut_summary);
- }
- shortcut.setOnPreferenceClickListener(preference -> {
- if (!ShortcutUtil.requestPinLaunchShortcut(() -> {
- App.getPreferences().edit().putBoolean("never_show_welcome", true).apply();
- parentFragment.showHint(R.string.settings_shortcut_pinned_hint, false);
- })) {
- parentFragment.showHint(R.string.settings_unsupported_pin_shortcut_summary, true);
- }
- return true;
- });
- }
-
- Preference backup = findPreference("backup");
- if (backup != null) {
- backup.setEnabled(installed);
- backup.setOnPreferenceClickListener(preference -> {
- LocalDateTime now = LocalDateTime.now();
- try {
- backupLauncher.launch(String.format(LocaleDelegate.getDefaultLocale(), "LSPosed_%s.lsp", now.toString()));
- return true;
- } catch (ActivityNotFoundException e) {
- parentFragment.showHint(R.string.enable_documentui, true);
- return false;
- }
- });
- }
-
- Preference restore = findPreference("restore");
- if (restore != null) {
- restore.setEnabled(installed);
- restore.setOnPreferenceClickListener(preference -> {
- try {
- restoreLauncher.launch(new String[]{"*/*"});
- return true;
- } catch (ActivityNotFoundException e) {
- parentFragment.showHint(R.string.enable_documentui, true);
- return false;
- }
- });
- }
-
- Preference theme = findPreference("dark_theme");
- if (theme != null) {
- theme.setOnPreferenceChangeListener((preference, newValue) -> {
- if (!App.getPreferences().getString("dark_theme", ThemeUtil.MODE_NIGHT_FOLLOW_SYSTEM).equals(newValue)) {
- AppCompatDelegate.setDefaultNightMode(ThemeUtil.getDarkTheme((String) newValue));
- }
- return true;
- });
- }
-
- Preference black_dark_theme = findPreference("black_dark_theme");
- if (black_dark_theme != null) {
- black_dark_theme.setOnPreferenceChangeListener((preference, newValue) -> {
- MainActivity activity = (MainActivity) getActivity();
- if (activity != null && ResourceUtils.isNightMode(getResources().getConfiguration())) {
- activity.restart();
- }
- return true;
- });
- }
-
- Preference primary_color = findPreference("theme_color");
- if (primary_color != null) {
- primary_color.setOnPreferenceChangeListener((preference, newValue) -> {
- MainActivity activity = (MainActivity) getActivity();
- if (activity != null) {
- activity.restart();
- }
- return true;
- });
- }
-
- MaterialSwitchPreference prefShowHiddenIcons = findPreference("show_hidden_icon_apps_enabled");
- if (prefShowHiddenIcons != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- if (ConfigManager.isBinderAlive()) {
- prefShowHiddenIcons.setEnabled(true);
- prefShowHiddenIcons.setOnPreferenceChangeListener((preference, newValue) -> ConfigManager.setHiddenIcon(!(boolean) newValue));
- }
- prefShowHiddenIcons.setChecked(Settings.Global.getInt(requireActivity().getContentResolver(), "show_hidden_icon_apps_enabled", 1) != 0);
- }
-
- MaterialSwitchPreference prefFollowSystemAccent = findPreference("follow_system_accent");
- if (prefFollowSystemAccent != null && DynamicColors.isDynamicColorAvailable()) {
- if (primary_color != null) {
- primary_color.setVisible(!prefFollowSystemAccent.isChecked());
- }
- prefFollowSystemAccent.setVisible(true);
- prefFollowSystemAccent.setOnPreferenceChangeListener((preference, newValue) -> {
- MainActivity activity = (MainActivity) getActivity();
- if (activity != null) {
- activity.restart();
- }
- return true;
- });
- }
-
- MaterialSwitchPreference prefDoH = findPreference("doh");
- if (prefDoH != null) {
- var dns = (CloudflareDNS) App.getOkHttpClient().dns();
- if (!dns.noProxy) {
- prefDoH.setEnabled(false);
- prefDoH.setVisible(false);
- var group = prefDoH.getParent();
- assert group != null;
- group.setVisible(false);
- }
- prefDoH.setOnPreferenceChangeListener((p, v) -> {
- dns.DoH = (boolean) v;
- return true;
- });
- }
-
- SimpleMenuPreference language = findPreference("language");
- if (language != null) {
- var tag = language.getValue();
- var userLocale = App.getLocale();
- var entries = new ArrayList();
- var lstLang = LangList.LOCALES;
- for (var lang : lstLang) {
- if (lang.equals(SYSTEM)) {
- entries.add(getString(rikka.core.R.string.follow_system));
- continue;
- }
- var locale = Locale.forLanguageTag(lang);
- entries.add(HtmlCompat.fromHtml(locale.getDisplayName(locale), HtmlCompat.FROM_HTML_MODE_LEGACY));
- }
- language.setEntries(entries.toArray(new CharSequence[0]));
- language.setEntryValues(lstLang);
- if (TextUtils.isEmpty(tag) || SYSTEM.equals(tag)) {
- language.setSummary(getString(rikka.core.R.string.follow_system));
- } else {
- var locale = Locale.forLanguageTag(tag);
- language.setSummary(!TextUtils.isEmpty(locale.getScript()) ? locale.getDisplayScript(userLocale) : locale.getDisplayName(userLocale));
- }
- language.setOnPreferenceChangeListener((preference, newValue) -> {
- var app = App.getInstance();
- var locale = App.getLocale((String) newValue);
- var res = app.getResources();
- var config = res.getConfiguration();
- config.setLocale(locale);
- LocaleDelegate.setDefaultLocale(locale);
- //noinspection deprecation
- res.updateConfiguration(config, res.getDisplayMetrics());
- MainActivity activity = (MainActivity) getActivity();
- if (activity != null) {
- activity.restart();
- }
- return true;
- });
- }
-
- Preference translation = findPreference("translation");
- if (translation != null) {
- translation.setOnPreferenceClickListener(preference -> {
- NavUtil.startURL(requireActivity(), "https://crowdin.com/project/lsposed_jingmatrix");
- return true;
- });
- translation.setSummary(getString(R.string.settings_translation_summary, getString(R.string.app_name)));
- }
-
- Preference translation_contributors = findPreference("translation_contributors");
- if (translation_contributors != null) {
- var translators = HtmlCompat.fromHtml(getString(R.string.translators), HtmlCompat.FROM_HTML_MODE_LEGACY);
- if (translators.toString().equals("null")) {
- translation_contributors.setVisible(false);
- } else {
- translation_contributors.setSummary(translators);
- }
- }
- SimpleMenuPreference channel = findPreference("update_channel");
- if (channel != null) {
- channel.setOnPreferenceChangeListener((preference, newValue) -> {
- var repoLoader = RepoLoader.getInstance();
- repoLoader.updateLatestVersion(String.valueOf(newValue));
- return true;
- });
- }
- }
-
- @NonNull
- @Override
- public RecyclerView onCreateRecyclerView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent, Bundle savedInstanceState) {
- BorderRecyclerView recyclerView = (BorderRecyclerView) super.onCreateRecyclerView(inflater, parent, savedInstanceState);
- RecyclerViewKt.fixEdgeEffect(recyclerView, false, true);
- recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> parentFragment.binding.appBar.setLifted(!top));
- var fragment = getParentFragment();
- if (fragment instanceof SettingsFragment settingsFragment) {
- View.OnClickListener l = v -> {
- settingsFragment.binding.appBar.setExpanded(true, true);
- recyclerView.smoothScrollToPosition(0);
- };
- settingsFragment.binding.toolbar.setOnClickListener(l);
- settingsFragment.binding.clickView.setOnClickListener(l);
- }
- return recyclerView;
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/widget/EmptyStateRecyclerView.java b/app/src/main/java/org/lsposed/manager/ui/widget/EmptyStateRecyclerView.java
deleted file mode 100644
index 02a6cd699..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/widget/EmptyStateRecyclerView.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.widget;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.text.Layout;
-import android.text.StaticLayout;
-import android.text.TextPaint;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.ConcatAdapter;
-
-import org.lsposed.manager.R;
-import org.lsposed.manager.util.SimpleStatefulAdaptor;
-
-import rikka.core.util.ResourceUtils;
-
-public class EmptyStateRecyclerView extends StatefulRecyclerView {
- private final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
- private final String emptyText;
-
- public EmptyStateRecyclerView(Context context) {
- this(context, null);
- }
-
- public EmptyStateRecyclerView(Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public EmptyStateRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- DisplayMetrics dm = context.getResources().getDisplayMetrics();
-
- paint.setColor(ResourceUtils.resolveColor(context.getTheme(), android.R.attr.textColorSecondary));
- paint.setTextSize(16f * dm.scaledDensity);
-
- emptyText = context.getString(R.string.list_empty);
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
- var adapter = getAdapter();
- if (adapter instanceof ConcatAdapter) {
- for (var a : ((ConcatAdapter) adapter).getAdapters()) {
- if (a instanceof EmptyStateAdapter) {
- adapter = a;
- break;
- }
- }
- }
- if (adapter instanceof EmptyStateAdapter && ((EmptyStateAdapter>) adapter).isLoaded() && adapter.getItemCount() == 0) {
- final int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
- final int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
-
- var textLayout = new StaticLayout(emptyText, paint, width, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false);
-
- canvas.save();
- canvas.translate(getPaddingLeft(), (height >> 1) + getPaddingTop() - (textLayout.getHeight() >> 1));
-
- textLayout.draw(canvas);
-
- canvas.restore();
- }
- }
-
- public abstract static class EmptyStateAdapter extends SimpleStatefulAdaptor {
- abstract public boolean isLoaded();
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/widget/ExpandableTextView.java b/app/src/main/java/org/lsposed/manager/ui/widget/ExpandableTextView.java
deleted file mode 100644
index 4b460950c..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/widget/ExpandableTextView.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- *
- */
-
-package org.lsposed.manager.ui.widget;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Typeface;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.text.Layout;
-import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.method.LinkMovementMethod;
-import android.text.style.ClickableSpan;
-import android.transition.TransitionManager;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-
-import com.google.android.material.textview.MaterialTextView;
-
-import org.lsposed.manager.R;
-
-public class ExpandableTextView extends MaterialTextView {
- private CharSequence text = null;
- private int nextLines = 0;
- private final int maxLines;
- private final SpannableString collapse;
- private final SpannableString expand;
- private final SpannableStringBuilder sb = new SpannableStringBuilder();
- private int lineCount = 0;
-
- public ExpandableTextView(Context context) {
- this(context, null);
- }
-
- public ExpandableTextView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ExpandableTextView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- maxLines = getMaxLines();
- collapse = new SpannableString(context.getString(R.string.collapse));
- ClickableSpan span = new ClickableSpan() {
- @Override
- public void onClick(@NonNull View widget) {
- TransitionManager.beginDelayedTransition((ViewGroup) getParent());
- setMaxLines(nextLines);
- ExpandableTextView.super.setText(text);
- }
-
- @Override
- public void updateDrawState(@NonNull TextPaint ds) {
- ds.setTypeface(Typeface.DEFAULT_BOLD);
- }
- };
- collapse.setSpan(span, 0, collapse.length(), 0);
- expand = new SpannableString(context.getString(R.string.expand));
- expand.setSpan(span, 0, expand.length(), 0);
- setMovementMethod(LinkMovementMethod.getInstance());
- }
-
- @Override
- public void setText(CharSequence text, BufferType type) {
- this.text = text;
- super.setText(text, type);
- }
-
- @Override
- public boolean onPreDraw() {
- this.getViewTreeObserver().removeOnPreDrawListener(this);
- if (lineCount == 0) {
- lineCount = getLayout().getLineCount();
- }
- if (lineCount > maxLines) {
- int hintTextOffsetEnd;
- if (maxLines == getMaxLines()) {
- nextLines = lineCount + 1;
- hintTextOffsetEnd = getLayout().getLineStart(getMaxLines() - 1);
- setTextWithSpan(text, hintTextOffsetEnd - 1, expand);
- } else if (nextLines == getMaxLines()) {
- nextLines = maxLines;
- hintTextOffsetEnd = getLayout().getLineStart(getMaxLines() - 1);
- setTextWithSpan(text, hintTextOffsetEnd, collapse);
- }
- }
- return super.onPreDraw();
- }
-
- private void setTextWithSpan(CharSequence text, int textOffsetEnd,
- SpannableString sbStr) {
- sb.clearSpans();
- sb.clear();
- sb.append(text, 0, textOffsetEnd);
- sb.append("\n");
- sb.append(sbStr);
- super.setText(sb, BufferType.NORMAL);
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- if (getLayout() != null) {
- lineCount = getLayout().getLineCount();
- }
- }
-
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public boolean onTouchEvent(@NonNull MotionEvent event) {
- Layout layout = this.getLayout();
- if (layout != null) {
- int line = layout.getLineForVertical((int) event.getY());
- int offset = layout.getOffsetForHorizontal(line, event.getX());
-
- if (getText() instanceof Spanned) {
- Spanned spanned = (Spanned) getText();
-
- ClickableSpan[] links = spanned.getSpans(offset, offset, ClickableSpan.class);
-
- if (links.length == 0) {
- return false;
- } else {
- return super.onTouchEvent(event);
- }
- }
- }
-
- return false;
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- Bundle bundle = new Bundle();
- bundle.putParcelable("superState", super.onSaveInstanceState());
- bundle.putInt("maxLines", getMaxLines());
- return bundle;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- if (state instanceof Bundle) {
- Bundle bundle = (Bundle) state;
- setMaxLines(bundle.getInt("maxLines"));
- state = bundle.getParcelable("superState");
- }
- super.onRestoreInstanceState(state);
- }
-
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/widget/LinkifyTextView.java b/app/src/main/java/org/lsposed/manager/ui/widget/LinkifyTextView.java
deleted file mode 100644
index 445413ec7..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/widget/LinkifyTextView.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.widget;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.text.Layout;
-import android.text.Spanned;
-import android.text.style.ClickableSpan;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import androidx.annotation.NonNull;
-
-public class LinkifyTextView extends androidx.appcompat.widget.AppCompatTextView {
-
- private ClickableSpan mCurrentSpan;
-
- public LinkifyTextView(Context context) {
- super(context);
- }
-
- public LinkifyTextView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public LinkifyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public ClickableSpan getCurrentSpan() {
- return mCurrentSpan;
- }
-
- public void clearCurrentSpan() {
- mCurrentSpan = null;
- }
-
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public boolean onTouchEvent(@NonNull MotionEvent event) {
- // Let the parent or grandparent of TextView to handles click action.
- // Otherwise click effect like ripple will not work, and if touch area
- // do not contain a url, the TextView will still get MotionEvent.
- // onTouchEven must be called with MotionEvent.ACTION_DOWN for each touch
- // action on it, so we analyze touched url here.
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- mCurrentSpan = null;
-
- if (getText() instanceof Spanned) {
- // Get this code from android.text.method.LinkMovementMethod.
- // Work fine !
- int x = (int) event.getX();
- int y = (int) event.getY();
-
- x -= getTotalPaddingLeft();
- y -= getTotalPaddingTop();
-
- x += getScrollX();
- y += getScrollY();
-
- Layout layout = getLayout();
- if (null != layout) {
- int line = layout.getLineForVertical(y);
- int off = layout.getOffsetForHorizontal(line, x);
-
- ClickableSpan[] spans = ((Spanned) getText()).getSpans(off, off, ClickableSpan.class);
-
- if (spans.length > 0) {
- mCurrentSpan = spans[0];
- }
- }
- }
- }
-
- return super.onTouchEvent(event);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/widget/ScrollWebView.java b/app/src/main/java/org/lsposed/manager/ui/widget/ScrollWebView.java
deleted file mode 100644
index 58757210f..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/widget/ScrollWebView.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2022 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.widget;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewParent;
-import android.webkit.WebView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-
-import rikka.widget.borderview.BorderRecyclerView;
-
-public class ScrollWebView extends WebView {
- public ScrollWebView(@NonNull Context context) {
- super(context);
- }
-
- public ScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- public ScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public ScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- var viewParent = findViewParentIfNeeds(this);
- if (viewParent != null) viewParent.requestDisallowInterceptTouchEvent(true);
- }
- return super.onTouchEvent(event);
- }
-
- @Override
- protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
- if (clampedX) {
- var viewParent = findViewParentIfNeeds(this);
- if (viewParent != null) viewParent.requestDisallowInterceptTouchEvent(false);
- }
- super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
- }
-
- private static ViewParent findViewParentIfNeeds(View v) {
- var parent = v.getParent();
- if (parent == null) return null;
- if (parent instanceof RecyclerView && !(parent instanceof BorderRecyclerView)) {
- return parent;
- } else if (parent instanceof View) {
- return findViewParentIfNeeds((View) parent);
- } else {
- return parent;
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/ui/widget/StatefulRecyclerView.java b/app/src/main/java/org/lsposed/manager/ui/widget/StatefulRecyclerView.java
deleted file mode 100644
index b017df3d6..000000000
--- a/app/src/main/java/org/lsposed/manager/ui/widget/StatefulRecyclerView.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2022 LSPosed Contributors
- */
-
-package org.lsposed.manager.ui.widget;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.util.AttributeSet;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.viewpager2.adapter.StatefulAdapter;
-
-import rikka.widget.borderview.BorderRecyclerView;
-
-public class StatefulRecyclerView extends BorderRecyclerView {
- public StatefulRecyclerView(@NonNull Context context) {
- super(context);
- }
-
- public StatefulRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- public StatefulRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
-
- @Override
- public Parcelable onSaveInstanceState() {
- Bundle bundle = new Bundle();
- bundle.putParcelable("superState", super.onSaveInstanceState());
- var adapter = getAdapter();
- if (adapter instanceof StatefulAdapter) {
- bundle.putParcelable("adaptor", ((StatefulAdapter) adapter).saveState());
- }
- return bundle;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- if (state instanceof Bundle) {
- Bundle bundle = (Bundle) state;
- super.onRestoreInstanceState(bundle.getParcelable("superState"));
- var adapter = getAdapter();
- if (adapter instanceof StatefulAdapter) {
- ((StatefulAdapter) adapter).restoreState(bundle.getParcelable("adaptor"));
- }
- } else {
- super.onRestoreInstanceState(state);
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/AccessibilityUtils.java b/app/src/main/java/org/lsposed/manager/util/AccessibilityUtils.java
deleted file mode 100644
index 65de7d6a7..000000000
--- a/app/src/main/java/org/lsposed/manager/util/AccessibilityUtils.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.lsposed.manager.util;
-
-import android.content.ContentResolver;
-import android.provider.Settings;
-
-public class AccessibilityUtils {
- public static boolean isAnimationEnabled(ContentResolver cr) {
- return !(Settings.Global.getFloat(cr, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f
- && Settings.Global.getFloat(cr, Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f) == 0.0f
- && Settings.Global.getFloat(cr, Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f) == 0.0f);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/AppIconModelLoader.java b/app/src/main/java/org/lsposed/manager/util/AppIconModelLoader.java
deleted file mode 100644
index fb65b0fda..000000000
--- a/app/src/main/java/org/lsposed/manager/util/AppIconModelLoader.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 2020 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.lsposed.manager.util;
-
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.graphics.Bitmap;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.Px;
-
-import com.bumptech.glide.Priority;
-import com.bumptech.glide.load.DataSource;
-import com.bumptech.glide.load.Options;
-import com.bumptech.glide.load.data.DataFetcher;
-import com.bumptech.glide.load.model.ModelLoader;
-import com.bumptech.glide.load.model.ModelLoaderFactory;
-import com.bumptech.glide.load.model.MultiModelLoaderFactory;
-import com.bumptech.glide.signature.ObjectKey;
-
-import org.lsposed.manager.App;
-
-import me.zhanghai.android.appiconloader.AppIconLoader;
-
-public class AppIconModelLoader implements ModelLoader {
- @NonNull
- private final AppIconLoader mLoader;
- @NonNull
- private final Context mContext;
-
- private AppIconModelLoader(@Px int iconSize, boolean shrinkNonAdaptiveIcons,
- @NonNull Context context) {
- mLoader = new AppIconLoader(iconSize, shrinkNonAdaptiveIcons, context);
- mContext = context;
- }
-
- @Override
- public boolean handles(@NonNull PackageInfo model) {
- return true;
- }
-
- @Nullable
- @Override
- public LoadData buildLoadData(@NonNull PackageInfo model, int width, int height,
- @NonNull Options options) {
- var warpApplicationInfo = new ApplicationInfo(model.applicationInfo);
- warpApplicationInfo.uid = warpApplicationInfo.uid % App.PER_USER_RANGE;
- var warpPackageInfo = new PackageInfo();
- warpPackageInfo.applicationInfo = warpApplicationInfo;
- warpPackageInfo.versionCode = model.versionCode;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- warpPackageInfo.setLongVersionCode(model.getLongVersionCode());
- }
- return new LoadData<>(new ObjectKey(AppIconLoader.getIconKey(warpPackageInfo, mContext)),
- new Fetcher(mLoader, warpApplicationInfo));
- }
-
- private static class Fetcher implements DataFetcher {
- @NonNull
- private final AppIconLoader mLoader;
- @NonNull
- private final ApplicationInfo mApplicationInfo;
-
- public Fetcher(@NonNull AppIconLoader loader, @NonNull ApplicationInfo applicationInfo) {
- mLoader = loader;
- mApplicationInfo = applicationInfo;
- }
-
- @Override
- public void loadData(@NonNull Priority priority,
- @NonNull DataCallback super Bitmap> callback) {
- try {
- Bitmap icon = mLoader.loadIcon(mApplicationInfo);
- callback.onDataReady(icon);
- } catch (Exception e) {
- callback.onLoadFailed(e);
- }
- }
-
- @Override
- public void cleanup() {
- }
-
- @Override
- public void cancel() {
- }
-
- @NonNull
- @Override
- public Class getDataClass() {
- return Bitmap.class;
- }
-
- @NonNull
- @Override
- public DataSource getDataSource() {
- return DataSource.LOCAL;
- }
- }
-
- public static class Factory implements ModelLoaderFactory {
- @Px
- private final int mIconSize;
- private final boolean mShrinkNonAdaptiveIcons;
- @NonNull
- private final Context mContext;
-
- public Factory(@Px int iconSize, boolean shrinkNonAdaptiveIcons, @NonNull Context context) {
- mIconSize = iconSize;
- mShrinkNonAdaptiveIcons = shrinkNonAdaptiveIcons;
- mContext = context.getApplicationContext();
- }
-
- @NonNull
- @Override
- public ModelLoader build(
- @NonNull MultiModelLoaderFactory multiFactory) {
- return new AppIconModelLoader(mIconSize, mShrinkNonAdaptiveIcons, mContext);
- }
-
- @Override
- public void teardown() {
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/AppModule.java b/app/src/main/java/org/lsposed/manager/util/AppModule.java
deleted file mode 100644
index c827ffd7a..000000000
--- a/app/src/main/java/org/lsposed/manager/util/AppModule.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.graphics.Bitmap;
-
-import androidx.annotation.NonNull;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.Registry;
-import com.bumptech.glide.annotation.GlideModule;
-import com.bumptech.glide.module.AppGlideModule;
-
-import org.lsposed.manager.R;
-
-@GlideModule
-public class AppModule extends AppGlideModule {
- @Override
- public boolean isManifestParsingEnabled() {
- return false;
- }
-
- @Override
- public void registerComponents(Context context, @NonNull Glide glide, Registry registry) {
- int iconSize = context.getResources().getDimensionPixelSize(R.dimen.app_icon_size);
- var factory = new AppIconModelLoader.Factory(iconSize, false, context);
- registry.prepend(PackageInfo.class, Bitmap.class, factory);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/BackupUtils.java b/app/src/main/java/org/lsposed/manager/util/BackupUtils.java
deleted file mode 100644
index f81801475..000000000
--- a/app/src/main/java/org/lsposed/manager/util/BackupUtils.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.net.Uri;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.lsposed.manager.App;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.adapters.ScopeAdapter;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.List;
-import java.util.zip.GZIPInputStream;
-import java.util.zip.GZIPOutputStream;
-
-import rikka.core.os.FileUtils;
-
-public class BackupUtils {
- private static final int VERSION = 2;
-
- public static void backup(Uri uri) throws JSONException, IOException {
- backup(uri, null);
- }
-
- public static void backup(Uri uri, String packageName) throws IOException, JSONException {
- JSONObject rootObject = new JSONObject();
- rootObject.put("version", VERSION);
- JSONArray modulesArray = new JSONArray();
- var modules = ModuleUtil.getInstance().getModules();
- if (modules == null) return;
- for (ModuleUtil.InstalledModule module : modules.values()) {
- if (packageName != null && !module.packageName.equals(packageName)) {
- continue;
- }
- JSONObject moduleObject = new JSONObject();
- moduleObject.put("enable", ModuleUtil.getInstance().isModuleEnabled(module.packageName));
- moduleObject.put("package", module.packageName);
- List scope = ConfigManager.getModuleScope(module.packageName);
- JSONArray scopeArray = new JSONArray();
- for (ScopeAdapter.ApplicationWithEquals s : scope) {
- JSONObject app = new JSONObject();
- app.put("package", s.packageName);
- app.put("userId", s.userId);
- scopeArray.put(app);
- }
- moduleObject.put("scope", scopeArray);
- modulesArray.put(moduleObject);
- }
- rootObject.put("modules", modulesArray);
- try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(App.getInstance().getContentResolver().openOutputStream(uri))) {
- gzipOutputStream.write(rootObject.toString().getBytes());
- }
- }
-
- public static void restore(Uri uri) throws JSONException, IOException {
- restore(uri, null);
- }
-
- public static void restore(Uri uri, String packageName) throws IOException, JSONException {
- try (GZIPInputStream gzipInputStream = new GZIPInputStream(App.getInstance().getContentResolver().openInputStream(uri), 32)) {
- StringBuilder string = new StringBuilder();
- try (var os = new ByteArrayOutputStream()) {
- FileUtils.copy(gzipInputStream, os);
- string.append(os);
- }
- gzipInputStream.close();
- JSONObject rootObject = new JSONObject(string.toString());
- int version = rootObject.getInt("version");
- if (version == VERSION || version == 1) {
- JSONArray modules = rootObject.getJSONArray("modules");
- for (int i = 0; i < modules.length(); i++) {
- JSONObject moduleObject = modules.getJSONObject(i);
- String name = moduleObject.getString("package");
- if (packageName != null && !name.equals(packageName)) {
- continue;
- }
- ModuleUtil.InstalledModule module = ModuleUtil.getInstance().getModule(name);
- if (module != null) {
- var enabled = moduleObject.getBoolean("enable");
- ModuleUtil.getInstance().setModuleEnabled(name, enabled);
- if (!enabled) continue;
- JSONArray scopeArray = moduleObject.getJSONArray("scope");
- HashSet scope = new HashSet<>();
- for (int j = 0; j < scopeArray.length(); j++) {
- if (version == VERSION) {
- JSONObject app = scopeArray.getJSONObject(j);
- scope.add(new ScopeAdapter.ApplicationWithEquals(app.getString("package"), app.getInt("userId")));
- } else {
- scope.add(new ScopeAdapter.ApplicationWithEquals(scopeArray.getString(j), 0));
- }
- }
- ConfigManager.setModuleScope(name, module.legacy, scope);
- }
- }
- } else {
- throw new IllegalArgumentException("Unknown backup file version");
- }
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/CloudflareDNS.java b/app/src/main/java/org/lsposed/manager/util/CloudflareDNS.java
deleted file mode 100644
index 5ab0dd17e..000000000
--- a/app/src/main/java/org/lsposed/manager/util/CloudflareDNS.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package org.lsposed.manager.util;
-
-import android.os.Build;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import org.lsposed.manager.App;
-
-import java.net.InetAddress;
-import java.net.Proxy;
-import java.net.ProxySelector;
-import java.net.UnknownHostException;
-import java.time.Duration;
-import java.util.List;
-
-import okhttp3.ConnectionSpec;
-import okhttp3.Dns;
-import okhttp3.HttpUrl;
-import okhttp3.OkHttpClient;
-import okhttp3.dnsoverhttps.DnsOverHttps;
-import okhttp3.internal.platform.Platform;
-
-public final class CloudflareDNS implements Dns {
-
- private static final HttpUrl url = HttpUrl.get("https://cloudflare-dns.com/dns-query");
- public boolean DoH = App.getPreferences().getBoolean("doh", false);
- public boolean noProxy = ProxySelector.getDefault().select(url.uri()).get(0) == Proxy.NO_PROXY;
- private final Dns cloudflare;
- // Set once the DoH resolver proves unreachable (e.g. Cloudflare blocked on
- // this network) so we stop paying its timeout on every subsequent lookup and
- // use the system resolver for the rest of the session.
- private volatile boolean dohUnavailable = false;
-
- public CloudflareDNS() {
- var trustManager = Platform.get().platformTrustManager();
- var tls = ConnectionSpec.RESTRICTED_TLS;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- //noinspection deprecation
- tls = new ConnectionSpec.Builder(tls)
- .supportsTlsExtensions(false)
- .build();
- }
- var builder = new DnsOverHttps.Builder()
- .resolvePrivateAddresses(true)
- .url(HttpUrl.get("https://cloudflare-dns.com/dns-query"))
- .client(new OkHttpClient.Builder()
- .cache(App.getOkHttpCache())
- .sslSocketFactory(new NoSniFactory(), trustManager)
- .connectionSpecs(List.of(tls))
- // Fail fast when the DoH endpoint is blocked so the
- // system-DNS fallback kicks in quickly instead of
- // stalling on the default 10s connect timeout.
- .connectTimeout(Duration.ofSeconds(3))
- .callTimeout(Duration.ofSeconds(5))
- .build());
- try {
- builder.bootstrapDnsHosts(List.of(
- InetAddress.getByName("1.1.1.1"),
- InetAddress.getByName("1.0.0.1"),
- InetAddress.getByName("2606:4700:4700::1111"),
- InetAddress.getByName("2606:4700:4700::1001")));
- } catch (UnknownHostException ignored) {
- }
- cloudflare = builder.build();
- }
-
- @NonNull
- @Override
- public List lookup(@NonNull String hostname) throws UnknownHostException {
- if (DoH && noProxy && !dohUnavailable) {
- try {
- return cloudflare.lookup(hostname);
- } catch (UnknownHostException e) {
- // The DoH resolver is unreachable on this network (e.g. Cloudflare
- // is blocked). Fall back to the system resolver so the app keeps
- // working instead of failing every lookup, and skip DoH for the
- // rest of the session.
- dohUnavailable = true;
- Log.w(App.TAG, "DoH resolver unreachable, falling back to system DNS for this session: " + e.getMessage());
- }
- }
- return SYSTEM.lookup(hostname);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/EmptyAccessibilityDelegate.java b/app/src/main/java/org/lsposed/manager/util/EmptyAccessibilityDelegate.java
deleted file mode 100644
index 6df1d9fdc..000000000
--- a/app/src/main/java/org/lsposed/manager/util/EmptyAccessibilityDelegate.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2022 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.os.Bundle;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeProvider;
-
-public class EmptyAccessibilityDelegate extends View.AccessibilityDelegate {
-
- @Override
- public void sendAccessibilityEvent(View host, int eventType) {
-
- }
-
- @Override
- public boolean performAccessibilityAction(View host, int action, Bundle args) {
- return true;
- }
-
- @Override
- public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
-
- }
-
- @Override
- public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
- return true;
- }
-
- @Override
- public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
-
- }
-
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
-
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
-
- }
-
- @Override
- public void addExtraDataToAccessibilityNodeInfo(View host, AccessibilityNodeInfo info, String extraDataKey, Bundle arguments) {
-
- }
-
- @Override
- public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) {
- return true;
- }
-
- @Override
- public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
- return null;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/ModuleUtil.java b/app/src/main/java/org/lsposed/manager/util/ModuleUtil.java
deleted file mode 100644
index 1fc50843d..000000000
--- a/app/src/main/java/org/lsposed/manager/util/ModuleUtil.java
+++ /dev/null
@@ -1,414 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.util.Pair;
-
-import org.lsposed.lspd.models.UserInfo;
-import org.lsposed.manager.App;
-import org.lsposed.manager.ConfigManager;
-import org.lsposed.manager.repo.RepoLoader;
-import org.lsposed.manager.repo.model.OnlineModule;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-import java.util.zip.ZipFile;
-
-public final class ModuleUtil {
- // xposedminversion below this
- public static int MIN_MODULE_VERSION = 2; // reject modules with
- private static ModuleUtil instance = null;
- private final PackageManager pm;
- private final Set listeners = ConcurrentHashMap.newKeySet();
- private HashSet enabledModules = new HashSet<>();
- private List users = new ArrayList<>();
- private Map, InstalledModule> installedModules = new HashMap<>();
- private boolean modulesLoaded = false;
-
- static final int MATCH_ANY_USER = 0x00400000; // PackageManager.MATCH_ANY_USER
-
- static final int MATCH_ALL_FLAGS = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_UNINSTALLED_PACKAGES | MATCH_ANY_USER;
-
- private ModuleUtil() {
- pm = App.getInstance().getPackageManager();
- }
-
- public boolean isModulesLoaded() {
- return modulesLoaded;
- }
-
- public static synchronized ModuleUtil getInstance() {
- if (instance == null) {
- instance = new ModuleUtil();
- App.getExecutorService().submit(instance::reloadInstalledModules);
- }
- return instance;
- }
-
- public static int extractIntPart(String str) {
- // minApiVersion and targetApiVersion are required of a module, but a third-party module
- // omitting one must not take down the module list.
- if (str == null) return 0;
- int result = 0, length = str.length();
- for (int offset = 0; offset < length; offset++) {
- char c = str.charAt(offset);
- if ('0' <= c && c <= '9')
- result = result * 10 + (c - '0');
- else
- break;
- }
- return result;
- }
-
- public static ZipFile getModernModuleApk(ApplicationInfo info) {
- String[] apks;
- if (info.splitSourceDirs != null) {
- apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1);
- apks[info.splitSourceDirs.length] = info.sourceDir;
- } else apks = new String[]{info.sourceDir};
- ZipFile zip = null;
- for (var apk : apks) {
- try {
- zip = new ZipFile(apk);
- if (zip.getEntry("META-INF/xposed/java_init.list") != null) {
- return zip;
- }
- zip.close();
- zip = null;
- } catch (IOException ignored) {
- }
- }
- return zip;
- }
-
- public static boolean isLegacyModule(ApplicationInfo info) {
- return info.metaData != null && info.metaData.containsKey("xposedminversion");
- }
-
- synchronized public void reloadInstalledModules() {
- modulesLoaded = false;
- if (!ConfigManager.isBinderAlive()) {
- modulesLoaded = true;
- return;
- }
-
- Map, InstalledModule> modules = new HashMap<>();
- var users = ConfigManager.getUsers();
- for (PackageInfo pkg : ConfigManager.getInstalledPackagesFromAllUsers(PackageManager.GET_META_DATA | MATCH_ALL_FLAGS, false)) {
- ApplicationInfo app = pkg.applicationInfo;
-
- var modernApk = getModernModuleApk(app);
- if (modernApk != null || isLegacyModule(app)) {
- modules.computeIfAbsent(Pair.create(pkg.packageName, app.uid / App.PER_USER_RANGE), k -> new InstalledModule(pkg, modernApk));
- }
- }
-
- installedModules = modules;
-
- this.users = users;
-
- enabledModules = new HashSet<>(Arrays.asList(ConfigManager.getEnabledModules()));
- modulesLoaded = true;
- listeners.forEach(ModuleListener::onModulesReloaded);
- }
-
- @Nullable
- public List getUsers() {
- return modulesLoaded ? users : null;
- }
-
- public InstalledModule reloadSingleModule(String packageName, int userId) {
- return reloadSingleModule(packageName, userId, false);
- }
-
- public InstalledModule reloadSingleModule(String packageName, int userId, boolean packageFullyRemoved) {
- if (packageFullyRemoved && isModuleEnabled(packageName)) {
- enabledModules.remove(packageName);
- listeners.forEach(ModuleListener::onModulesReloaded);
- }
- PackageInfo pkg;
-
- try {
- pkg = ConfigManager.getPackageInfo(packageName, PackageManager.GET_META_DATA, userId);
- } catch (NameNotFoundException e) {
- InstalledModule old = installedModules.remove(Pair.create(packageName, userId));
- if (old != null) listeners.forEach(i -> i.onSingleModuleReloaded(old));
- return null;
- }
-
- ApplicationInfo app = pkg.applicationInfo;
- var modernApk = getModernModuleApk(app);
- if (modernApk != null || isLegacyModule(app)) {
- InstalledModule module = new InstalledModule(pkg, modernApk);
- installedModules.put(Pair.create(packageName, userId), module);
- listeners.forEach(i -> i.onSingleModuleReloaded(module));
- return module;
- } else {
- InstalledModule old = installedModules.remove(Pair.create(packageName, userId));
- if (old != null) listeners.forEach(i -> i.onSingleModuleReloaded(old));
- return null;
- }
- }
-
- @Nullable
- public InstalledModule getModule(String packageName, int userId) {
- return modulesLoaded ? installedModules.get(Pair.create(packageName, userId)) : null;
- }
-
- @Nullable
- public InstalledModule getModule(String packageName) {
- return getModule(packageName, 0);
- }
-
- @Nullable
- synchronized public Map, InstalledModule> getModules() {
- return modulesLoaded ? installedModules : null;
- }
-
- public boolean setModuleEnabled(String packageName, boolean enabled) {
- if (!ConfigManager.setModuleEnabled(packageName, enabled)) {
- return false;
- }
- if (enabled) {
- enabledModules.add(packageName);
- } else {
- enabledModules.remove(packageName);
- }
- return true;
- }
-
- public boolean isModuleEnabled(String packageName) {
- return enabledModules.contains(packageName);
- }
-
- public int getEnabledModulesCount() {
- return modulesLoaded ? enabledModules.size() : -1;
- }
-
- public void addListener(ModuleListener listener) {
- listeners.add(listener);
- }
-
- public void removeListener(ModuleListener listener) {
- listeners.remove(listener);
- }
-
- public interface ModuleListener {
- /**
- * Called whenever one (previously or now) installed module has been
- * reloaded
- */
- default void onSingleModuleReloaded(InstalledModule module) {
-
- }
-
- default void onModulesReloaded() {
-
- }
- }
-
- public class InstalledModule {
- //private static final int FLAG_FORWARD_LOCK = 1 << 29;
- public final int userId;
- public final String packageName;
- public final String versionName;
- public final long versionCode;
- public final boolean legacy;
- public final int minVersion;
- public final int targetVersion;
- public final boolean staticScope;
- public final long installTime;
- public final long updateTime;
- public final ApplicationInfo app;
- public final PackageInfo pkg;
- private String appName; // loaded lazily
- private String description; // loaded lazily
- private List scopeList; // loaded lazily
-
- private InstalledModule(PackageInfo pkg, ZipFile modernModuleApk) {
- app = pkg.applicationInfo;
- this.pkg = pkg;
- userId = pkg.applicationInfo.uid / App.PER_USER_RANGE;
- packageName = pkg.packageName;
- versionName = pkg.versionName;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- versionCode = pkg.versionCode;
- } else {
- versionCode = pkg.getLongVersionCode();
- }
- installTime = pkg.firstInstallTime;
- updateTime = pkg.lastUpdateTime;
- legacy = modernModuleApk == null;
-
- if (legacy) {
- Object minVersionRaw = app.metaData.get("xposedminversion");
- if (minVersionRaw instanceof Integer) {
- minVersion = (Integer) minVersionRaw;
- } else if (minVersionRaw instanceof String) {
- minVersion = extractIntPart((String) minVersionRaw);
- } else {
- minVersion = 0;
- }
- targetVersion = minVersion; // legacy modules don't have a target version
- staticScope = false;
- } else {
- int minVersion = 100;
- int targetVersion = 100;
- boolean staticScope = false;
- try (modernModuleApk) {
- var propEntry = modernModuleApk.getEntry("META-INF/xposed/module.prop");
- if (propEntry != null) {
- try {
- var prop = new Properties();
- prop.load(modernModuleApk.getInputStream(propEntry));
- minVersion = extractIntPart(prop.getProperty("minApiVersion"));
- targetVersion = extractIntPart(prop.getProperty("targetApiVersion"));
- staticScope = TextUtils.equals(prop.getProperty("staticScope"), "true");
- } catch (IllegalArgumentException e) {
- // Properties.load rejects a malformed unicode escape, and nothing read
- // out of the file before that point can be trusted either, so the
- // module reports no version rather than the defaults above.
- minVersion = 0;
- targetVersion = 0;
- Log.e(App.TAG, "Malformed module.prop in " + pkg.packageName, e);
- }
- }
- var scopeEntry = modernModuleApk.getEntry("META-INF/xposed/scope.list");
- if (scopeEntry != null) {
- try (var reader = new BufferedReader(new InputStreamReader(modernModuleApk.getInputStream(scopeEntry)))) {
- scopeList = reader.lines().collect(Collectors.toList());
- }
- } else {
- scopeList = Collections.emptyList();
- }
- } catch (IOException | OutOfMemoryError | IllegalArgumentException e) {
- // Properties.load is documented to throw IllegalArgumentException for a
- // malformed unicode escape. Uncaught, one such module.prop leaves the user
- // with an empty module list instead of one unreadable entry.
- Log.e(App.TAG, "Error while reading modern module APK", e);
- }
- this.minVersion = minVersion;
- this.targetVersion = targetVersion;
- this.staticScope = staticScope;
- }
- }
-
- public boolean isInstalledOnExternalStorage() {
- return (app.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0;
- }
-
- public String getAppName() {
- if (appName == null)
- appName = app.loadLabel(pm).toString();
- return appName;
- }
-
- public String getDescription() {
- if (this.description != null) return this.description;
- String descriptionTmp = "";
- if (legacy) {
- Object descriptionRaw = app.metaData.get("xposeddescription");
- if (descriptionRaw instanceof String) {
- descriptionTmp = ((String) descriptionRaw).trim();
- } else if (descriptionRaw instanceof Integer) {
- try {
- int resId = (Integer) descriptionRaw;
- if (resId != 0)
- descriptionTmp = pm.getResourcesForApplication(app).getString(resId).trim();
- } catch (Exception ignored) {
- }
- }
- } else {
- var des = app.loadDescription(pm);
- if (des != null) descriptionTmp = des.toString();
- }
- this.description = descriptionTmp;
- return this.description;
- }
-
- public List getScopeList() {
- if (scopeList != null) return scopeList;
- List list = null;
- try {
- int scopeListResourceId = app.metaData.getInt("xposedscope");
- if (scopeListResourceId != 0) {
- list = Arrays.asList(pm.getResourcesForApplication(app).getStringArray(scopeListResourceId));
- } else {
- String scopeListString = app.metaData.getString("xposedscope");
- if (scopeListString != null)
- list = Arrays.asList(scopeListString.split(";"));
- }
- } catch (Exception ignored) {
- }
- if (list == null) {
- OnlineModule module = RepoLoader.getInstance().getOnlineModule(packageName);
- if (module != null && module.getScope() != null) {
- list = module.getScope();
- }
- }
- if (list != null) {
- //For historical reasons, legacy modules use the opposite name.
- //https://github.com/rovo89/XposedBridge/commit/6b49688c929a7768f3113b4c65b429c7a7032afa
- list.replaceAll(s ->
- switch (s) {
- case "android" -> "system";
- case "system" -> "android";
- default -> s;
- }
- );
- scopeList = list;
- }
- return scopeList;
- }
-
- public PackageInfo getPackageInfo() {
- return pkg;
- }
-
- @NonNull
- @Override
- public String toString() {
- return getAppName();
- }
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/NavUtil.java b/app/src/main/java/org/lsposed/manager/util/NavUtil.java
deleted file mode 100644
index 1848173db..000000000
--- a/app/src/main/java/org/lsposed/manager/util/NavUtil.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.app.Activity;
-import android.content.ActivityNotFoundException;
-import android.net.Uri;
-import android.widget.Toast;
-
-import androidx.browser.customtabs.CustomTabColorSchemeParams;
-import androidx.browser.customtabs.CustomTabsIntent;
-
-import rikka.core.util.ResourceUtils;
-
-public final class NavUtil {
-
- public static void startURL(Activity activity, Uri uri) {
- CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
- customTabsIntent.setShowTitle(true);
- CustomTabColorSchemeParams params = new CustomTabColorSchemeParams.Builder()
- .setToolbarColor(ResourceUtils.resolveColor(activity.getTheme(), android.R.attr.colorBackground))
- .setNavigationBarColor(ResourceUtils.resolveColor(activity.getTheme(), android.R.attr.navigationBarColor))
- .setNavigationBarDividerColor(0)
- .build();
- customTabsIntent.setDefaultColorSchemeParams(params);
- boolean night = ResourceUtils.isNightMode(activity.getResources().getConfiguration());
- customTabsIntent.setColorScheme(night ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT);
- try {
- customTabsIntent.build().launchUrl(activity, uri);
- } catch (ActivityNotFoundException ignored) {
- Toast.makeText(activity, uri.toString(), Toast.LENGTH_SHORT).show();
- }
- }
-
- public static void startURL(Activity activity, String url) {
- startURL(activity, Uri.parse(url));
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/NoSniFactory.java b/app/src/main/java/org/lsposed/manager/util/NoSniFactory.java
deleted file mode 100644
index 034c6096b..000000000
--- a/app/src/main/java/org/lsposed/manager/util/NoSniFactory.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package org.lsposed.manager.util;
-
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.Socket;
-
-import javax.net.ssl.SSLSocketFactory;
-
-public final class NoSniFactory extends SSLSocketFactory {
- private static final SSLSocketFactory defaultFactory = (SSLSocketFactory) getDefault();
- @SuppressWarnings("deprecation")
- private static final android.net.SSLCertificateSocketFactory openSSLSocket =
- (android.net.SSLCertificateSocketFactory) android.net.SSLCertificateSocketFactory
- .getDefault(1000);
-
- @Override
- public String[] getDefaultCipherSuites() {
- return defaultFactory.getDefaultCipherSuites();
- }
-
- @Override
- public String[] getSupportedCipherSuites() {
- return defaultFactory.getSupportedCipherSuites();
- }
-
- @Override
- public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
- return config(defaultFactory.createSocket(s, host, port, autoClose));
- }
-
- @Override
- public Socket createSocket(String host, int port) throws IOException {
- return config(defaultFactory.createSocket(host, port));
- }
-
- @Override
- public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
- return config(defaultFactory.createSocket(host, port, localHost, localPort));
- }
-
- @Override
- public Socket createSocket(InetAddress host, int port) throws IOException {
- return config(defaultFactory.createSocket(host, port));
- }
-
- @Override
- public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
- return config(defaultFactory.createSocket(address, port, localAddress, localPort));
- }
-
- private Socket config(Socket socket) {
- try {
- openSSLSocket.setHostname(socket, null);
- openSSLSocket.setUseSessionTickets(socket, true);
- } catch (IllegalArgumentException ignored) {
- }
- return socket;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/ShortcutUtil.java b/app/src/main/java/org/lsposed/manager/util/ShortcutUtil.java
deleted file mode 100644
index 2c49eb956..000000000
--- a/app/src/main/java/org/lsposed/manager/util/ShortcutUtil.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2022 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.annotation.SuppressLint;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.IntentSender;
-import android.content.pm.PackageManager;
-import android.content.pm.ShortcutInfo;
-import android.content.pm.ShortcutManager;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Build;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.R;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
-public class ShortcutUtil {
- private static final String SHORTCUT_ID = "org.lsposed.manager.shortcut";
-
- private static Bitmap getBitmap(Context context, int id) {
- var r = context.getResources();
- var res = r.getDrawable(id, context.getTheme());
- if (res instanceof BitmapDrawable) {
- return ((BitmapDrawable) res).getBitmap();
- } else {
- if (res instanceof AdaptiveIconDrawable) {
- var layers = new Drawable[]{((AdaptiveIconDrawable) res).getBackground(),
- ((AdaptiveIconDrawable) res).getForeground()};
- res = new LayerDrawable(layers);
- }
- var bitmap = Bitmap.createBitmap(res.getIntrinsicWidth(),
- res.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
- var canvas = new Canvas(bitmap);
- res.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- res.draw(canvas);
- return bitmap;
- }
- }
-
- private static Intent getLaunchIntent(Context context) {
- var pm = context.getPackageManager();
- var pkg = context.getPackageName();
- var intent = pm.getLaunchIntentForPackage(pkg);
- if (intent == null) {
- try {
- var pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES);
- if (pkgInfo.activities != null) {
- for (var activityInfo : pkgInfo.activities) {
- if (activityInfo.processName.equals(activityInfo.packageName)) {
- intent = new Intent(Intent.ACTION_MAIN);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setComponent(new ComponentName(pkg, activityInfo.name));
- break;
- }
- }
- }
- } catch (PackageManager.NameNotFoundException ignored) {
- }
- }
- if (intent != null) {
- var categories = intent.getCategories();
- if (categories != null) {
- categories.clear();
- }
- intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER");
- intent.setPackage(pkg);
- }
- return intent;
- }
-
- @SuppressLint("InlinedApi")
- private static IntentSender registerReceiver(Context context, Runnable task) {
- if (task == null) return null;
- var uuid = UUID.randomUUID().toString();
- var filter = new IntentFilter(uuid);
- var permission = "android.permission.CREATE_USERS";
- var receiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context c, Intent intent) {
- if (!uuid.equals(intent.getAction())) return;
- context.unregisterReceiver(this);
- task.run();
- }
- };
- context.registerReceiver(receiver, filter, permission,
- null/* main thread */, Context.RECEIVER_EXPORTED);
-
- var intent = new Intent(uuid);
- int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
- return PendingIntent.getBroadcast(context, 0, intent, flags).getIntentSender();
- }
-
- private static ShortcutInfo.Builder getShortcutBuilder(Context context) {
- var builder = new ShortcutInfo.Builder(context, SHORTCUT_ID)
- .setShortLabel(context.getString(R.string.app_name))
- .setIntent(getLaunchIntent(context))
- .setIcon(Icon.createWithAdaptiveBitmap(getBitmap(context,
- R.drawable.ic_launcher)));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- var activity = new ComponentName(context.getPackageName(),
- "android.app.AppDetailsActivity");
- builder.setActivity(activity);
- }
- return builder;
- }
-
- public static boolean isRequestPinShortcutSupported(Context context) throws RuntimeException {
- var sm = context.getSystemService(ShortcutManager.class);
- return sm.isRequestPinShortcutSupported();
- }
-
- public static boolean requestPinLaunchShortcut(Runnable afterPinned) {
- if (!App.isParasitic) throw new RuntimeException();
- var context = App.getInstance();
- var sm = context.getSystemService(ShortcutManager.class);
- if (!sm.isRequestPinShortcutSupported()) return false;
- return sm.requestPinShortcut(getShortcutBuilder(context).build(),
- registerReceiver(context, afterPinned));
- }
-
- public static boolean updateShortcut() {
- if (!isLaunchShortcutPinned()) return false;
- var context = App.getInstance();
- var sm = context.getSystemService(ShortcutManager.class);
- List shortcutInfoList = new ArrayList<>();
- shortcutInfoList.add(getShortcutBuilder(context).build());
- return sm.updateShortcuts(shortcutInfoList);
- }
-
- public static boolean isLaunchShortcutPinned() {
- var context = App.getInstance();
- var sm = context.getSystemService(ShortcutManager.class);
- for (var info : sm.getPinnedShortcuts()) {
- if (SHORTCUT_ID.equals(info.getId())) {
- return true;
- }
- }
- return false;
- }
-
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/SimpleStatefulAdaptor.java b/app/src/main/java/org/lsposed/manager/util/SimpleStatefulAdaptor.java
deleted file mode 100644
index a4db646d9..000000000
--- a/app/src/main/java/org/lsposed/manager/util/SimpleStatefulAdaptor.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2022 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.util.SparseArray;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.viewpager2.adapter.StatefulAdapter;
-
-import java.util.HashMap;
-import java.util.List;
-
-public abstract class SimpleStatefulAdaptor extends RecyclerView.Adapter implements StatefulAdapter {
- HashMap> states = new HashMap<>();
- protected RecyclerView rv = null;
-
- public SimpleStatefulAdaptor() {
- setStateRestorationPolicy(StateRestorationPolicy.PREVENT_WHEN_EMPTY);
- }
-
- @Override
- @CallSuper
- public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
- rv = recyclerView;
- super.onAttachedToRecyclerView(recyclerView);
- }
-
- @Override
- public void onViewRecycled(@NonNull T holder) {
- saveStateOf(holder);
- super.onViewRecycled(holder);
- }
-
- @CallSuper
- @Override
- public final void onBindViewHolder(@NonNull T holder, int position, @NonNull List payloads) {
- var state = states.remove(holder.getItemId());
- if (state != null) {
- holder.itemView.restoreHierarchyState(state);
- }
- onBindViewHolder(holder, position);
- }
-
- private void saveStateOf(@NonNull RecyclerView.ViewHolder holder) {
- var state = new SparseArray();
- holder.itemView.saveHierarchyState(state);
- states.put(holder.getItemId(), state);
- }
-
- @NonNull
- public Parcelable saveState() {
- for (int childCount = rv.getChildCount(), i = 0; i < childCount; ++i) {
- saveStateOf(rv.getChildViewHolder(rv.getChildAt(i)));
- }
-
- var out = new Bundle();
- for (var state : states.entrySet()) {
- var item = new Bundle();
- for (int i = 0; i < state.getValue().size(); ++i) {
- item.putParcelable(String.valueOf(state.getValue().keyAt(i)), state.getValue().valueAt(i));
- }
- out.putParcelable(String.valueOf(state.getKey()), item);
- }
- return out;
- }
-
- @Override
- public void restoreState(@NonNull Parcelable savedState) {
- if (savedState instanceof Bundle) {
- for (var stateKey : ((Bundle) savedState).keySet()) {
- var array = new SparseArray();
- var state = ((Bundle) savedState).getParcelable(stateKey);
- if (state instanceof Bundle) {
- for (var itemKey : ((Bundle) state).keySet()) {
- var item = ((Bundle) state).getParcelable(itemKey);
- array.put(Integer.parseInt(itemKey), item);
- }
- }
- states.put(Long.parseLong(stateKey), array);
- }
- }
- }
-
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/ThemeUtil.java b/app/src/main/java/org/lsposed/manager/util/ThemeUtil.java
deleted file mode 100644
index d27acd48d..000000000
--- a/app/src/main/java/org/lsposed/manager/util/ThemeUtil.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import androidx.annotation.StyleRes;
-import androidx.appcompat.app.AppCompatDelegate;
-
-import com.google.android.material.color.DynamicColors;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.R;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import rikka.core.util.ResourceUtils;
-
-public class ThemeUtil {
- private static final Map colorThemeMap = new HashMap<>();
- private static final SharedPreferences preferences;
-
- public static final String MODE_NIGHT_FOLLOW_SYSTEM = "MODE_NIGHT_FOLLOW_SYSTEM";
- public static final String MODE_NIGHT_NO = "MODE_NIGHT_NO";
- public static final String MODE_NIGHT_YES = "MODE_NIGHT_YES";
-
- static {
- preferences = App.getPreferences();
- colorThemeMap.put("SAKURA", R.style.ThemeOverlay_MaterialSakura);
- colorThemeMap.put("MATERIAL_RED", R.style.ThemeOverlay_MaterialRed);
- colorThemeMap.put("MATERIAL_PINK", R.style.ThemeOverlay_MaterialPink);
- colorThemeMap.put("MATERIAL_PURPLE", R.style.ThemeOverlay_MaterialPurple);
- colorThemeMap.put("MATERIAL_DEEP_PURPLE", R.style.ThemeOverlay_MaterialDeepPurple);
- colorThemeMap.put("MATERIAL_INDIGO", R.style.ThemeOverlay_MaterialIndigo);
- colorThemeMap.put("MATERIAL_BLUE", R.style.ThemeOverlay_MaterialBlue);
- colorThemeMap.put("MATERIAL_LIGHT_BLUE", R.style.ThemeOverlay_MaterialLightBlue);
- colorThemeMap.put("MATERIAL_CYAN", R.style.ThemeOverlay_MaterialCyan);
- colorThemeMap.put("MATERIAL_TEAL", R.style.ThemeOverlay_MaterialTeal);
- colorThemeMap.put("MATERIAL_GREEN", R.style.ThemeOverlay_MaterialGreen);
- colorThemeMap.put("MATERIAL_LIGHT_GREEN", R.style.ThemeOverlay_MaterialLightGreen);
- colorThemeMap.put("MATERIAL_LIME", R.style.ThemeOverlay_MaterialLime);
- colorThemeMap.put("MATERIAL_YELLOW", R.style.ThemeOverlay_MaterialYellow);
- colorThemeMap.put("MATERIAL_AMBER", R.style.ThemeOverlay_MaterialAmber);
- colorThemeMap.put("MATERIAL_ORANGE", R.style.ThemeOverlay_MaterialOrange);
- colorThemeMap.put("MATERIAL_DEEP_ORANGE", R.style.ThemeOverlay_MaterialDeepOrange);
- colorThemeMap.put("MATERIAL_BROWN", R.style.ThemeOverlay_MaterialBrown);
- colorThemeMap.put("MATERIAL_BLUE_GREY", R.style.ThemeOverlay_MaterialBlueGrey);
- }
-
- private static final String THEME_DEFAULT = "DEFAULT";
- private static final String THEME_BLACK = "BLACK";
-
- private static boolean isBlackNightTheme() {
- return preferences.getBoolean("black_dark_theme", false);
- }
-
- public static boolean isSystemAccent() {
- return DynamicColors.isDynamicColorAvailable() && preferences.getBoolean("follow_system_accent", true);
- }
-
- public static String getNightTheme(Context context) {
- if (isBlackNightTheme()
- && ResourceUtils.isNightMode(context.getResources().getConfiguration()))
- return THEME_BLACK;
-
- return THEME_DEFAULT;
- }
-
- @StyleRes
- public static int getNightThemeStyleRes(Context context) {
- switch (getNightTheme(context)) {
- case THEME_BLACK:
- return R.style.ThemeOverlay_Black;
- case THEME_DEFAULT:
- default:
- return R.style.ThemeOverlay;
- }
- }
-
- public static String getColorTheme() {
- if (isSystemAccent()) {
- return "SYSTEM";
- }
- return preferences.getString("theme_color", "COLOR_BLUE");
- }
-
- @StyleRes
- public static int getColorThemeStyleRes() {
- Integer theme = colorThemeMap.get(getColorTheme());
- if (theme == null) {
- return R.style.ThemeOverlay_MaterialBlue;
- }
- return theme;
- }
-
- public static int getDarkTheme(String mode) {
- switch (mode) {
- case MODE_NIGHT_FOLLOW_SYSTEM:
- default:
- return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
- case MODE_NIGHT_YES:
- return AppCompatDelegate.MODE_NIGHT_YES;
- case MODE_NIGHT_NO:
- return AppCompatDelegate.MODE_NIGHT_NO;
- }
- }
-
- public static int getDarkTheme() {
- return getDarkTheme(preferences.getString("dark_theme", MODE_NIGHT_FOLLOW_SYSTEM));
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java b/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java
deleted file mode 100644
index 10d2191b0..000000000
--- a/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2022 LSPosed Contributors
- */
-
-package org.lsposed.manager.util;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
-import org.lsposed.manager.App;
-import org.lsposed.manager.BuildConfig;
-import org.lsposed.manager.ConfigManager;
-
-import java.io.File;
-import java.io.IOException;
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.util.Locale;
-
-import okhttp3.Call;
-import okhttp3.Callback;
-import okhttp3.Request;
-import okhttp3.Response;
-import okio.Okio;
-
-public class UpdateUtil {
- public static void loadRemoteVersion() {
- var request = new Request.Builder()
- .url("https://api.github.com/repos/JingMatrix/LSPosed/releases/latest")
- .addHeader("Accept", "application/vnd.github.v3+json")
- .build();
- var callback = new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (!response.isSuccessful()) return;
- var body = response.body();
- if (body == null) return;
- try {
- var info = JsonParser.parseReader(body.charStream()).getAsJsonObject();
- var notes = info.get("body").getAsString();
- var assetsArray = info.getAsJsonArray("assets");
- for (var assets : assetsArray) {
- checkAssets(assets.getAsJsonObject(), notes);
- }
- } catch (Throwable t) {
- Log.e(App.TAG, t.getMessage(), t);
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull IOException e) {
- Log.e(App.TAG, "loadRemoteVersion: " + e.getMessage());
- var pref = App.getPreferences();
- if (pref.getBoolean("checked", false)) return;
- pref.edit().putBoolean("checked", true).apply();
- }
- };
- App.getOkHttpClient().newCall(request).enqueue(callback);
- }
-
- private static void checkAssets(JsonObject assets, String releaseNotes) {
- var pref = App.getPreferences();
- var name = assets.get("name").getAsString();
- var splitName = name.split("-");
- pref.edit()
- .putInt("latest_version", Integer.parseInt(splitName[2]))
- .putLong("latest_check", Instant.now().getEpochSecond())
- .putString("release_notes", releaseNotes)
- .putString("zip_file", null)
- .putBoolean("checked", true)
- .apply();
- var updatedAt = Instant.parse(assets.get("updated_at").getAsString());
- var downloadUrl = assets.get("browser_download_url").getAsString();
- var zipTime = pref.getLong("zip_time", 0);
- if (!updatedAt.equals(Instant.ofEpochSecond(zipTime))) {
- var zip = downloadNewZipSync(downloadUrl, name);
- var size = assets.get("size").getAsLong();
- if (zip != null && zip.length() == size) {
- pref.edit()
- .putLong("zip_time", updatedAt.getEpochSecond())
- .putString("zip_file", zip.getAbsolutePath())
- .apply();
- }
- }
- }
-
- public static boolean needUpdate() {
- var pref = App.getPreferences();
- if (!pref.getBoolean("checked", false)) return false;
- var now = Instant.now();
- var buildTime = Instant.ofEpochSecond(BuildConfig.BUILD_TIME);
- var check = pref.getLong("latest_check", 0);
- if (check > 0) {
- var checkTime = Instant.ofEpochSecond(check);
- if (checkTime.atOffset(ZoneOffset.UTC).plusDays(30).toInstant().isBefore(now))
- return true;
- var code = pref.getInt("latest_version", 0);
- return code > BuildConfig.VERSION_CODE;
- }
- return buildTime.atOffset(ZoneOffset.UTC).plusDays(30).toInstant().isBefore(now);
- }
-
- @Nullable
- private static File downloadNewZipSync(String url, String name) {
- var request = new Request.Builder().url(url).build();
- var zip = new File(App.getInstance().getCacheDir(), name);
- try (Response response = App.getOkHttpClient().newCall(request).execute()) {
- var body = response.body();
- if (!response.isSuccessful() || body == null) return null;
- try (var source = body.source();
- var sink = Okio.buffer(Okio.sink(zip))) {
- sink.writeAll(source);
- }
- } catch (IOException e) {
- Log.e(App.TAG, "downloadNewZipSync: " + e.getMessage());
- return null;
- }
- return zip;
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/chrome/CustomTabsURLSpan.java b/app/src/main/java/org/lsposed/manager/util/chrome/CustomTabsURLSpan.java
deleted file mode 100644
index 927ac914a..000000000
--- a/app/src/main/java/org/lsposed/manager/util/chrome/CustomTabsURLSpan.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.util.chrome;
-
-import android.app.Activity;
-import android.text.style.URLSpan;
-import android.view.View;
-
-import org.lsposed.manager.util.NavUtil;
-
-public class CustomTabsURLSpan extends URLSpan {
-
- private final Activity activity;
-
- public CustomTabsURLSpan(Activity activity, String url) {
- super(url);
- this.activity = activity;
- }
-
- @Override
- public void onClick(View widget) {
- String url = getURL();
- NavUtil.startURL(activity, url);
- }
-}
diff --git a/app/src/main/java/org/lsposed/manager/util/chrome/LinkTransformationMethod.java b/app/src/main/java/org/lsposed/manager/util/chrome/LinkTransformationMethod.java
deleted file mode 100644
index bf3a3b366..000000000
--- a/app/src/main/java/org/lsposed/manager/util/chrome/LinkTransformationMethod.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * This file is part of LSPosed.
- *
- * LSPosed is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LSPosed is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LSPosed. If not, see .
- *
- * Copyright (C) 2020 EdXposed Contributors
- * Copyright (C) 2021 LSPosed Contributors
- */
-
-package org.lsposed.manager.util.chrome;
-
-import android.app.Activity;
-import android.graphics.Rect;
-import android.text.Spannable;
-import android.text.Spanned;
-import android.text.method.TransformationMethod;
-import android.text.style.URLSpan;
-import android.view.View;
-import android.widget.TextView;
-
-public class LinkTransformationMethod implements TransformationMethod {
-
- private final Activity activity;
-
- public LinkTransformationMethod(Activity activity) {
- this.activity = activity;
- }
-
- @Override
- public CharSequence getTransformation(CharSequence source, View view) {
- if (view instanceof TextView) {
- TextView textView = (TextView) view;
- if (textView.getText() == null || !(textView.getText() instanceof Spannable)) {
- return source;
- }
- Spannable text = (Spannable) textView.getText();
- URLSpan[] spans = text.getSpans(0, textView.length(), URLSpan.class);
- for (int i = spans.length - 1; i >= 0; i--) {
- URLSpan oldSpan = spans[i];
- int start = text.getSpanStart(oldSpan);
- int end = text.getSpanEnd(oldSpan);
- String url = oldSpan.getURL();
- text.removeSpan(oldSpan);
- text.setSpan(new CustomTabsURLSpan(activity, url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- return text;
- }
- return source;
- }
-
- @Override
- public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect) {
- }
-}
diff --git a/app/src/main/res/anim/fragment_enter.xml b/app/src/main/res/anim/fragment_enter.xml
deleted file mode 100644
index 729cbef69..000000000
--- a/app/src/main/res/anim/fragment_enter.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/anim/fragment_enter_pop.xml b/app/src/main/res/anim/fragment_enter_pop.xml
deleted file mode 100644
index 7e863ffbc..000000000
--- a/app/src/main/res/anim/fragment_enter_pop.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/anim/fragment_exit.xml b/app/src/main/res/anim/fragment_exit.xml
deleted file mode 100644
index c4ccf0dc4..000000000
--- a/app/src/main/res/anim/fragment_exit.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/anim/fragment_exit_pop.xml b/app/src/main/res/anim/fragment_exit_pop.xml
deleted file mode 100644
index efb2c235e..000000000
--- a/app/src/main/res/anim/fragment_exit_pop.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_assignment_checkable.xml b/app/src/main/res/drawable/ic_assignment_checkable.xml
deleted file mode 100644
index 953092d06..000000000
--- a/app/src/main/res/drawable/ic_assignment_checkable.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_attach_file.xml b/app/src/main/res/drawable/ic_attach_file.xml
deleted file mode 100644
index 919b7fcd6..000000000
--- a/app/src/main/res/drawable/ic_attach_file.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_add_24.xml b/app/src/main/res/drawable/ic_baseline_add_24.xml
deleted file mode 100644
index 16b01c6dd..000000000
--- a/app/src/main/res/drawable/ic_baseline_add_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
deleted file mode 100644
index 801a1dead..000000000
--- a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_assignment_24.xml b/app/src/main/res/drawable/ic_baseline_assignment_24.xml
deleted file mode 100644
index 64b5cff24..000000000
--- a/app/src/main/res/drawable/ic_baseline_assignment_24.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_chat_24.xml b/app/src/main/res/drawable/ic_baseline_chat_24.xml
deleted file mode 100644
index 21beb622b..000000000
--- a/app/src/main/res/drawable/ic_baseline_chat_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_extension_24.xml b/app/src/main/res/drawable/ic_baseline_extension_24.xml
deleted file mode 100644
index db1669bd8..000000000
--- a/app/src/main/res/drawable/ic_baseline_extension_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_get_app_24.xml b/app/src/main/res/drawable/ic_baseline_get_app_24.xml
deleted file mode 100644
index 7fd5a906b..000000000
--- a/app/src/main/res/drawable/ic_baseline_get_app_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_home_24.xml b/app/src/main/res/drawable/ic_baseline_home_24.xml
deleted file mode 100644
index f4ab22a5d..000000000
--- a/app/src/main/res/drawable/ic_baseline_home_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_info_24.xml b/app/src/main/res/drawable/ic_baseline_info_24.xml
deleted file mode 100644
index 8edf9d4b4..000000000
--- a/app/src/main/res/drawable/ic_baseline_info_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_search_24.xml b/app/src/main/res/drawable/ic_baseline_search_24.xml
deleted file mode 100644
index c1ea83ba3..000000000
--- a/app/src/main/res/drawable/ic_baseline_search_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_settings_24.xml b/app/src/main/res/drawable/ic_baseline_settings_24.xml
deleted file mode 100644
index 99f112297..000000000
--- a/app/src/main/res/drawable/ic_baseline_settings_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_baseline_settings_backup_restore_24.xml b/app/src/main/res/drawable/ic_baseline_settings_backup_restore_24.xml
deleted file mode 100644
index 123d5230f..000000000
--- a/app/src/main/res/drawable/ic_baseline_settings_backup_restore_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_extension_checkable.xml b/app/src/main/res/drawable/ic_extension_checkable.xml
deleted file mode 100644
index 940258ae8..000000000
--- a/app/src/main/res/drawable/ic_extension_checkable.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_get_app_checkable.xml b/app/src/main/res/drawable/ic_get_app_checkable.xml
deleted file mode 100644
index e08a1f9b0..000000000
--- a/app/src/main/res/drawable/ic_get_app_checkable.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_home_checkable.xml b/app/src/main/res/drawable/ic_home_checkable.xml
deleted file mode 100644
index 7a087431e..000000000
--- a/app/src/main/res/drawable/ic_home_checkable.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_down.xml b/app/src/main/res/drawable/ic_keyboard_arrow_down.xml
deleted file mode 100644
index 0bc9590ac..000000000
--- a/app/src/main/res/drawable/ic_keyboard_arrow_down.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml
deleted file mode 100644
index c16bb8f93..000000000
--- a/app/src/main/res/drawable/ic_launcher.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
deleted file mode 100644
index 8e75c220e..000000000
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_round.xml b/app/src/main/res/drawable/ic_launcher_round.xml
deleted file mode 100644
index 496302eb9..000000000
--- a/app/src/main/res/drawable/ic_launcher_round.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
- -
-
-
-
-
-
- -
-
-
-
diff --git a/app/src/main/res/drawable/ic_open_in_browser.xml b/app/src/main/res/drawable/ic_open_in_browser.xml
deleted file mode 100644
index 3c04742fd..000000000
--- a/app/src/main/res/drawable/ic_open_in_browser.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_android_24.xml b/app/src/main/res/drawable/ic_outline_android_24.xml
deleted file mode 100644
index 47196d1ea..000000000
--- a/app/src/main/res/drawable/ic_outline_android_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_app_shortcut_24.xml b/app/src/main/res/drawable/ic_outline_app_shortcut_24.xml
deleted file mode 100644
index 0373d4cb5..000000000
--- a/app/src/main/res/drawable/ic_outline_app_shortcut_24.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_assignment_24.xml b/app/src/main/res/drawable/ic_outline_assignment_24.xml
deleted file mode 100644
index fb83e0d85..000000000
--- a/app/src/main/res/drawable/ic_outline_assignment_24.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_dark_mode_24.xml b/app/src/main/res/drawable/ic_outline_dark_mode_24.xml
deleted file mode 100644
index 184c7e5dd..000000000
--- a/app/src/main/res/drawable/ic_outline_dark_mode_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_dns_24.xml b/app/src/main/res/drawable/ic_outline_dns_24.xml
deleted file mode 100644
index d6ba21455..000000000
--- a/app/src/main/res/drawable/ic_outline_dns_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_extension_24.xml b/app/src/main/res/drawable/ic_outline_extension_24.xml
deleted file mode 100644
index 5850b5655..000000000
--- a/app/src/main/res/drawable/ic_outline_extension_24.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_format_color_fill_24.xml b/app/src/main/res/drawable/ic_outline_format_color_fill_24.xml
deleted file mode 100644
index 1d404e9a2..000000000
--- a/app/src/main/res/drawable/ic_outline_format_color_fill_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_get_app_24.xml b/app/src/main/res/drawable/ic_outline_get_app_24.xml
deleted file mode 100644
index 2b66ca700..000000000
--- a/app/src/main/res/drawable/ic_outline_get_app_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_groups_24.xml b/app/src/main/res/drawable/ic_outline_groups_24.xml
deleted file mode 100644
index 9b3bd0fb8..000000000
--- a/app/src/main/res/drawable/ic_outline_groups_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_home_24.xml b/app/src/main/res/drawable/ic_outline_home_24.xml
deleted file mode 100644
index 2c9291c47..000000000
--- a/app/src/main/res/drawable/ic_outline_home_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_invert_colors_24.xml b/app/src/main/res/drawable/ic_outline_invert_colors_24.xml
deleted file mode 100644
index 0b76592cb..000000000
--- a/app/src/main/res/drawable/ic_outline_invert_colors_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_language_24.xml b/app/src/main/res/drawable/ic_outline_language_24.xml
deleted file mode 100644
index be985e9cc..000000000
--- a/app/src/main/res/drawable/ic_outline_language_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_merge_type_24.xml b/app/src/main/res/drawable/ic_outline_merge_type_24.xml
deleted file mode 100644
index 33c6e830c..000000000
--- a/app/src/main/res/drawable/ic_outline_merge_type_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_palette_24.xml b/app/src/main/res/drawable/ic_outline_palette_24.xml
deleted file mode 100644
index a70d1a2e8..000000000
--- a/app/src/main/res/drawable/ic_outline_palette_24.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_restore_24.xml b/app/src/main/res/drawable/ic_outline_restore_24.xml
deleted file mode 100644
index 02b8dce82..000000000
--- a/app/src/main/res/drawable/ic_outline_restore_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_settings_24.xml b/app/src/main/res/drawable/ic_outline_settings_24.xml
deleted file mode 100644
index 82bddbe8e..000000000
--- a/app/src/main/res/drawable/ic_outline_settings_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_shield_24.xml b/app/src/main/res/drawable/ic_outline_shield_24.xml
deleted file mode 100644
index 2b98bb986..000000000
--- a/app/src/main/res/drawable/ic_outline_shield_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_speaker_notes_24.xml b/app/src/main/res/drawable/ic_outline_speaker_notes_24.xml
deleted file mode 100644
index 7ffe2e971..000000000
--- a/app/src/main/res/drawable/ic_outline_speaker_notes_24.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_outline_translate_24.xml b/app/src/main/res/drawable/ic_outline_translate_24.xml
deleted file mode 100644
index 1b2fb9ca5..000000000
--- a/app/src/main/res/drawable/ic_outline_translate_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_round_bug_report_24.xml b/app/src/main/res/drawable/ic_round_bug_report_24.xml
deleted file mode 100644
index d9faada4f..000000000
--- a/app/src/main/res/drawable/ic_round_bug_report_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_round_check_circle_24.xml b/app/src/main/res/drawable/ic_round_check_circle_24.xml
deleted file mode 100644
index 6fe7cdd1f..000000000
--- a/app/src/main/res/drawable/ic_round_check_circle_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_round_error_outline_24.xml b/app/src/main/res/drawable/ic_round_error_outline_24.xml
deleted file mode 100644
index e51d888e8..000000000
--- a/app/src/main/res/drawable/ic_round_error_outline_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_round_settings_24.xml b/app/src/main/res/drawable/ic_round_settings_24.xml
deleted file mode 100644
index 5cda2d061..000000000
--- a/app/src/main/res/drawable/ic_round_settings_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_round_update_24.xml b/app/src/main/res/drawable/ic_round_update_24.xml
deleted file mode 100644
index 2b469b675..000000000
--- a/app/src/main/res/drawable/ic_round_update_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_round_warning_24.xml b/app/src/main/res/drawable/ic_round_warning_24.xml
deleted file mode 100644
index 685f45378..000000000
--- a/app/src/main/res/drawable/ic_round_warning_24.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml
deleted file mode 100644
index 325732501..000000000
--- a/app/src/main/res/drawable/ic_save.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_settings_checkable.xml b/app/src/main/res/drawable/ic_settings_checkable.xml
deleted file mode 100644
index ec9892de9..000000000
--- a/app/src/main/res/drawable/ic_settings_checkable.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/shortcut_ic_logs.xml b/app/src/main/res/drawable/shortcut_ic_logs.xml
deleted file mode 100644
index 889f7d8b3..000000000
--- a/app/src/main/res/drawable/shortcut_ic_logs.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/shortcut_ic_modules.xml b/app/src/main/res/drawable/shortcut_ic_modules.xml
deleted file mode 100644
index e27f6c6d7..000000000
--- a/app/src/main/res/drawable/shortcut_ic_modules.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/shortcut_ic_repo.xml b/app/src/main/res/drawable/shortcut_ic_repo.xml
deleted file mode 100644
index 439d29521..000000000
--- a/app/src/main/res/drawable/shortcut_ic_repo.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/shortcut_ic_settings.xml b/app/src/main/res/drawable/shortcut_ic_settings.xml
deleted file mode 100644
index 68fbf4792..000000000
--- a/app/src/main/res/drawable/shortcut_ic_settings.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/simple_menu_background.xml b/app/src/main/res/drawable/simple_menu_background.xml
deleted file mode 100644
index 45b614f7d..000000000
--- a/app/src/main/res/drawable/simple_menu_background.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
diff --git a/app/src/main/res/layout-sw600dp/activity_main.xml b/app/src/main/res/layout-sw600dp/activity_main.xml
deleted file mode 100644
index 4a622f174..000000000
--- a/app/src/main/res/layout-sw600dp/activity_main.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 7e956da97..000000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml
deleted file mode 100644
index 2139cb2d6..000000000
--- a/app/src/main/res/layout/dialog_about.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/dialog_item.xml b/app/src/main/res/layout/dialog_item.xml
deleted file mode 100644
index 0df9f1145..000000000
--- a/app/src/main/res/layout/dialog_item.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/dialog_title.xml b/app/src/main/res/layout/dialog_title.xml
deleted file mode 100644
index ffdbb2382..000000000
--- a/app/src/main/res/layout/dialog_title.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/fragment_app_list.xml b/app/src/main/res/layout/fragment_app_list.xml
deleted file mode 100644
index 0600e2354..000000000
--- a/app/src/main/res/layout/fragment_app_list.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_compile_dialog.xml b/app/src/main/res/layout/fragment_compile_dialog.xml
deleted file mode 100644
index 98fe0e235..000000000
--- a/app/src/main/res/layout/fragment_compile_dialog.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
deleted file mode 100644
index f1f2f5b98..000000000
--- a/app/src/main/res/layout/fragment_home.xml
+++ /dev/null
@@ -1,372 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_pager.xml b/app/src/main/res/layout/fragment_pager.xml
deleted file mode 100644
index a5fa82aca..000000000
--- a/app/src/main/res/layout/fragment_pager.xml
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_repo.xml b/app/src/main/res/layout/fragment_repo.xml
deleted file mode 100644
index 54a023813..000000000
--- a/app/src/main/res/layout/fragment_repo.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
deleted file mode 100644
index cab64c869..000000000
--- a/app/src/main/res/layout/fragment_settings.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_log_textview.xml b/app/src/main/res/layout/item_log_textview.xml
deleted file mode 100644
index 7363d2761..000000000
--- a/app/src/main/res/layout/item_log_textview.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/item_master_switch.xml b/app/src/main/res/layout/item_master_switch.xml
deleted file mode 100644
index 4b3029ca1..000000000
--- a/app/src/main/res/layout/item_master_switch.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/layout/item_module.xml b/app/src/main/res/layout/item_module.xml
deleted file mode 100644
index 6c0c59f8a..000000000
--- a/app/src/main/res/layout/item_module.xml
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_onlinemodule.xml b/app/src/main/res/layout/item_onlinemodule.xml
deleted file mode 100644
index 482f5b119..000000000
--- a/app/src/main/res/layout/item_onlinemodule.xml
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_repo_loadmore.xml b/app/src/main/res/layout/item_repo_loadmore.xml
deleted file mode 100644
index a77ec59df..000000000
--- a/app/src/main/res/layout/item_repo_loadmore.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_repo_readme.xml b/app/src/main/res/layout/item_repo_readme.xml
deleted file mode 100644
index fee5011e8..000000000
--- a/app/src/main/res/layout/item_repo_readme.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_repo_recyclerview.xml b/app/src/main/res/layout/item_repo_recyclerview.xml
deleted file mode 100644
index b4bda2229..000000000
--- a/app/src/main/res/layout/item_repo_recyclerview.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/item_repo_release.xml b/app/src/main/res/layout/item_repo_release.xml
deleted file mode 100644
index 3a307148b..000000000
--- a/app/src/main/res/layout/item_repo_release.xml
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_repo_title_description.xml b/app/src/main/res/layout/item_repo_title_description.xml
deleted file mode 100644
index a2df47dc6..000000000
--- a/app/src/main/res/layout/item_repo_title_description.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_scope_footer.xml b/app/src/main/res/layout/item_scope_footer.xml
deleted file mode 100644
index 4b18cb41a..000000000
--- a/app/src/main/res/layout/item_scope_footer.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/preference_recyclerview.xml b/app/src/main/res/layout/preference_recyclerview.xml
deleted file mode 100644
index d0f3985ff..000000000
--- a/app/src/main/res/layout/preference_recyclerview.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout/scrollable_dialog.xml b/app/src/main/res/layout/scrollable_dialog.xml
deleted file mode 100644
index c0cab916a..000000000
--- a/app/src/main/res/layout/scrollable_dialog.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/swiperefresh_recyclerview.xml b/app/src/main/res/layout/swiperefresh_recyclerview.xml
deleted file mode 100644
index badeaad53..000000000
--- a/app/src/main/res/layout/swiperefresh_recyclerview.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/menu-sw600dp/navigation_menu.xml b/app/src/main/res/menu-sw600dp/navigation_menu.xml
deleted file mode 100644
index 6362d1264..000000000
--- a/app/src/main/res/menu-sw600dp/navigation_menu.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/context_menu_modules.xml b/app/src/main/res/menu/context_menu_modules.xml
deleted file mode 100644
index e126ec147..000000000
--- a/app/src/main/res/menu/context_menu_modules.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_app_item.xml b/app/src/main/res/menu/menu_app_item.xml
deleted file mode 100644
index 1cb39221b..000000000
--- a/app/src/main/res/menu/menu_app_item.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_app_list.xml b/app/src/main/res/menu/menu_app_list.xml
deleted file mode 100644
index 280fa02b5..000000000
--- a/app/src/main/res/menu/menu_app_list.xml
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_home.xml b/app/src/main/res/menu/menu_home.xml
deleted file mode 100644
index fcaeb5b23..000000000
--- a/app/src/main/res/menu/menu_home.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_logs.xml b/app/src/main/res/menu/menu_logs.xml
deleted file mode 100644
index 98fe53d9d..000000000
--- a/app/src/main/res/menu/menu_logs.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_modules.xml b/app/src/main/res/menu/menu_modules.xml
deleted file mode 100644
index 4b8cbd3fc..000000000
--- a/app/src/main/res/menu/menu_modules.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_repo.xml b/app/src/main/res/menu/menu_repo.xml
deleted file mode 100644
index 387c34446..000000000
--- a/app/src/main/res/menu/menu_repo.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_repo_item.xml b/app/src/main/res/menu/menu_repo_item.xml
deleted file mode 100644
index 32e81b8cb..000000000
--- a/app/src/main/res/menu/menu_repo_item.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/menu/navigation_menu.xml b/app/src/main/res/menu/navigation_menu.xml
deleted file mode 100644
index bf38a5589..000000000
--- a/app/src/main/res/menu/navigation_menu.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/navigation/main_nav.xml b/app/src/main/res/navigation/main_nav.xml
deleted file mode 100644
index f855a2f65..000000000
--- a/app/src/main/res/navigation/main_nav.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/navigation/modules_nav.xml b/app/src/main/res/navigation/modules_nav.xml
deleted file mode 100644
index 6d07808b5..000000000
--- a/app/src/main/res/navigation/modules_nav.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/navigation/repo_nav.xml b/app/src/main/res/navigation/repo_nav.xml
deleted file mode 100644
index 2202d968c..000000000
--- a/app/src/main/res/navigation/repo_nav.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml
deleted file mode 100644
index 76548ffbf..000000000
--- a/app/src/main/res/values-af/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Oorsig
- Modules
-
- - %d module enabled
- - %d module enabled
-
- Logs
- Settings
- Terugvoer of voorstel
- Oor
- Rapporteer probleem
- Bewaarplek
- Alle modules op datum
- Published at %s
- Opgedateer op %s
-
- - %d module opgradeerbaar
- - %d modules opgradeerbaar
-
- Sluit aan by ons %2$s -kanaal]]>
- null
- Installeer2
- Tik om LSPosed te installeer
- Not installed1
- LSPosed is nie geïnstalleer nie
- Activated
- Partially activated
- SEPolicy is nie behoorlik gelaai nie
- Rapporteer dit asseblief aan Magisk ontwikkelaar.]]>
- Stelselraamwerk-inspuiting het misluk
- Magisk of sommige Magisk-modules van lae gehalte. Probeer asseblief om Magisk-modules anders as Riru en LSPosed te deaktiveer of dien volledige log aan ontwikkelaars in.]]>
- Stelselstut is verkeerd
- Modules kan soms ongeldig word.]]>
- Need to update
- Please install the latest version of LSPosed
- API version
- Framework version
- Bestuurder pakket naam
- Stelsel weergawe
- Toestel
- Stelsel ABI
- Dex Optimizer Wrapper
- Geaktiveer
- Nie geaktiveer nie
- Ondersteun
- Ongesteun
- Android-weergawe nie tevrede nie
- Het neergestort
- Montering het misluk
- SELinux is permissief
- SELinux-beleid is verkeerd
- Dateer LSPosed op
- Bevestig om LSPosed op te dateer? Hierdie toestel sal herlaai nadat die opdatering voltooi is
- Gekopieer na knipbord
-
- Welkom by LSPosed
- Jy gebruik die parasitiese bestuurder, wat kortpad kan skep of steeds oopmaak vanaf kennisgewing.
- Jy gebruik die parasitiese bestuurder, wat kan oopmaak vanaf kennisgewing.
- Skep kortpad
- Moet nooit wys nie
- Parasitiese Bestuurder Aanbeveel
- LSPosed ondersteun nou stelselparasitering om opsporing te vermy, jy kan parasitiese bestuurder oopmaak vanaf kennisgewing. Dit word aanbeveel om die huidige toepassing te verwyder.
-
- Stoor
- Uitgebreide logs
- Modules
- Stoor tans logboek, wag asseblief
- Logs gestoor
- Kon nie stoor nie:\n%s
- Vee logboek nou uit
- Log is suksesvol uitgevee.
- Blaai na bo
- Laai…
- Blaai na onder
- Herlaai
- Kon nie die logboek skoonmaak nie
- Woordomhulsel
- Uitgebreide logboek geaktiveer
- Uitgebreide logboek gedeaktiveer
-
- (geen beskrywing verskaf nie)
- Hierdie module vereis \'n nuwer Xposed weergawe (%d) en kan dus nie geaktiveer word nie
- This module is designed for a newer Xposed version (%d) and thus some functionalities may not work
- Hierdie module spesifiseer nie die Xposed-weergawe wat dit benodig nie.
- Hierdie module is geskep vir Xposed weergawe %1$d, maar as gevolg van onversoenbare veranderinge in weergawe %2$d, is dit gedeaktiveer
- Hierdie module kan nie gelaai word nie omdat dit op die SD-kaart geïnstalleer is, skuif dit asseblief na interne berging
- Deïnstalleer
- Module enabled
- Kyk in Repo
- Wil jy hierdie module deïnstalleer?
- Deïnstalleer %1$s
- Deïnstalleer onsuksesvol
- %d module enabled
- Het %1$s by gebruiker %2$sgevoeg
- %d module enabled
- Installeer op gebruiker %s
- Wil jy %1$s op gebruiker %2$sinstalleer? Dit word aanbeveel om met die hand te installeer, om installasie via LSPosed te dwing, kan probleme veroorsaak.
- uitbrei
- inval
-
- Heroptimaliseer
- Optimaliseer…
- Optimering voltooi
- Begin dit
- Optimalisering het misluk: terugkeerwaarde is leeg
- Optimalisering het misluk:
- Aansoeknaam
- Pakketnaam
- Installeer tyd
- Dateer tyd op
- Omgekeerde
- Stelseltoepassings
- Sorteer
- Aktiveer module
- Jy het geen toepassing gekies nie. Aanhou?
- Speletjies
- Modules
- Kon nie omvanglys stoor nie
- weergawe: %1$s
- Aanbeveel
- Jy het geen toepassing gekies nie. Kies aanbevole programme?
- Kies aanbevole programme?
- Xposed-module is nog nie geaktiveer nie
- Aanbeveel
- Opdatering beskikbaar: %1$s
- Module %s is gedeaktiveer aangesien geen toepassing gekies is nie.
- Stelselraamwerk
- Ondersteuning
- Ondersteuning
- Herstel
- Forseer om te stop
- Forseer om te stop?
- As jy \'n program dwing om te stop, kan dit dalk wangedra.
- Herselflaai word vereis vir hierdie verandering om van toepassing te wees
- Herlaai
- Versteek
-
- Bekyk in \'n ander toepassing
- App inligting
- ¯\\\\_(ツ)_\/¯\nNiks hier nie
-
- Raamwerk
- Deaktiveer verbose logs
- Rapporteer kwessies versoek om verbose logs in te sluit
- Swart donker tema
- Gebruik die suiwer swart tema as donker tema geaktiveer is
- Tema
- Friends en herstel
- Rugsteunmodulelys en omvanglyste.
- Herstel modulelys en omvanglyste.
- Ondersteuning
- Kon nie rugsteun nie:\n%s
- Aktiveer asseblief DocumentUI
- Herstel
- Kon nie herstel nie:\n%s
- Netwerk
- DNS oor HTTPS
- Oplossing DNS-vergiftiging in sommige lande
- Tema kleur
- Stelsel tema kleur
- Dwing programme om lanseerder-ikone te wys
- Ná Android 10 word programme nie toegelaat om hul lanseerder-ikone te versteek nie. Skakel die skakelaar af om hierdie stelselkenmerk te deaktiveer.
- Stelsel
- Taal
- Vertaling bydraers
- Neem deel aan vertaling
- Help ons om %s in jou taal te vertaal
- Skep \'n kortpad wat parasitiese bestuurder kan oopmaak
- Shortcut pinned
- Die huidige versteklanseerder ondersteun nie penkortpaaie nie
- Statuskennisgewing
- Wys \'n kennisgewing wat parasitiese bestuurder kan oopmaak
- Dateer kanaal op
- Stabiel
- Beta
- Nag bou
-
- Lees my
- Vrystellings
- Info
- Tuisblad
- Bronkode
- Medewerkers
- Bates
- Maak oop in blaaier
- Wys ouer weergawes
- Geen vrystelling meer nie
- Kon nie module repo laai nie: %s
- Eers opgradeerbaar
- Geïnstalleer
-
- - %d aflaai
- - %d aflaaie
-
-
- Sakura
- Rooi
- Pienk
- Pers
- Diep pers
- Indigo
- Blou
- Ligblou
- Siaan
- Blauwgroen
- Groen
- Ligte groen
- Lemmetjie
- Geel
- Amber
- Oranje
- Diep oranje
- Bruin
- Blou grys
-
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
deleted file mode 100644
index 21d4339ec..000000000
--- a/app/src/main/res/values-ar/strings.xml
+++ /dev/null
@@ -1,250 +0,0 @@
-
-
-
-
- ملخص
- الوحدات
-
- - %d وحدة مفعلة
- - %d وحدة مفعلة
- - %d وحدة مفعلة
- - %d وحدات مفعلة
- - %d وحدة مفعلة
- - %d وحدة مفعلة
-
- السجلات
- الإعدادات
- ملاحظات أو اقتراحات
- عنّ
- الإبلاغ عن مشكلة
- المُستودع
- جميع الوحدات محدثة
- تم نشرها في %s
- تَم تحديثها في %s
-
- - %d وحدة قابلة للترقية
- - %d وحدة قابلة للترقية
- - %d وحدة قابلة للترقية
- - %d وحدات قابلة للترقية
- - %d وحدة قابلة للترقية
- - %d وحدة قابلة للترقية
-
- انضم إلى قناتنا %2$s]]>
- Mahmoud Abd El-Hamed (7TM)
- تثبيت
- انقر لتثبيت LSPosed
- غير مثبت
- LSPosed غير مثبت
- مُفعّل
- مفعل جزئياً
- لم يتم تحميل SEPolicy بشكل صحيح
- الرجاء الإبلاغ عن هذا إلى ماجيسك مطور النظام.]]>
- فشل حقن إطار عمل النظام
- Magisk أو بعض وحدات Magisk منخفضة الجودة. الرجاء محاولة تعطيل وحدات Magisk خلاف Riru وLSPosed أو إرسال سجل كامل للمطورين.]]>
- نظام prop غير صحيح
- قد تبطل الوحدات أحيانا.]]>
- تحتاج إلى التحديث
- يرجى تثبيت أحدث إصدار من LSPosed
- نصائح لمطور الوحدة
- يرجى تعطيل تحسينات النشر على Android Studio، أو استخدام الأمر `gradlew installDebug` للتثبيت. وإلا فلن يتم تحديث ملف Apk للوحدة.
- إصدار API
- إصدار إطار العمل
- Lاسم حزمة المديرo
- إصدار النظام
- الجهاز
- نظام ABI
- غلاف Dex المحسّن
- مفعل
- غير مفعل
- مدعوم
- غير متوافق
- نسخة أندرويد غير راضية
- تعطّل
- فشل التحميل
- SELinux متساهل
- سياسة SELinux غير صحيحة
- تحديث LSPosed
- تأكيد تحديث LSPosed؟ سيتم إعـادة تشغيل هذا الجهاز بعد اكتمال التحديث
- تم النسخ إلى الحافظة
-
- مرحباً بك في LSPosed
- أنت تستخدم مدير الطفيليات، الذي يمكنه إنشاء اختصار أو لا يزال مفتوحا من الإشعارات.
- أنت تستخدم المدير الطفيلي، الذي يمكن فتحه من الإشعار.
- إنشاء إختصار
- لا تظهر أبداً
- مدير طفيلي موصي به
- يدعم LSPosed الآن تطهير النظام لتجنب الكشف، يمكنك فتح مدير الطفيليات من الإشعار. من المستحسن إلغاء تثبيت التطبيق الحالي.
-
- احفظ
- سجلات مفصّلة
- سجلات الوحدات
- حفظ السجل ، يرجى الانتظار
- تم حفظ السجلات
- فشل الحفظ:\n%s
- مسح السجل الآن
- تم مسح السجل بنجاح.
- التمرير لأعلى
- جارٍ التحميل…
- التمرير لأسفل
- إعادة التحميل
- فشل مسح السجل
- التفاف الكلمات
- تم تفعيل السجل المفصّل
- تم تعطيل السجل المفصّل
-
- (لم يتم تقديم وصف)
- هذه الوحدة تتطلب إصدار Xposed الأحدث (%d) لذا لا يمكن تفعيلها
- تم تصميم هذه الوحدة لإصدار Xposed أحدث (%d) وبالتالي قد لا تعمل بعض الوظائف
- لا تحدد هذه الوحدة إصدار Xposed الذي تحتاجه.
- تم إنشاء هذه الوحدة لإصدار Xposed %1$d ، ولكن بسبب التغييرات غير المتوافقة في الإصدار %2$d، فقد تم تعطيلها
- لا يمكن تحميل هذه الوحدة لأنها مثبتة على بطاقة الذاكرة، يرجى نقلها إلى مساحة التخزين الداخلية
- إلغاء التثبيت
- إعدادات الوحدة
- عرض في المستودع
- هل تريد إلغاء تثبيت هذه الوحدة؟
- تم إلغاء تثبيت %1$s
- فشل إلغاء التثبيت
- إضافة وحدة للمستخدم
- تم إضافة %1$s للمستخدم %2$s
- فشل إضافة الوحدة
- تثبيت للمستخدم %s
- هل ترغب في تثبيت %1$s للمستخدم %2$s؟ ينصح بالتثبيت يدوياً، قد يسبب إجبار التثبيت عبر LSPosed مشكلات.
- توسّع
- انهيار
-
- إعادة تحسين
- تحسين…
- اكتمل التحسين
- تشغيله
- فشل التحسين: قيمة الإرجاع فارغة
- فشل التحسين:
- أسم التطبيق
- أسم الحُزْمَة
- وقت التثبيت
- وقت التحديث
- عكسي
- تطبيقات النظام
- ترتيب
- تفعيل الوحدة
- أنت لم تحدد أي تطبيق. المتابعة؟
- ألعاب
- وحدات
- فشل في حفظ قائمة النطاق
- الإصدار: %1$s
- مُوصى به
- أنت لم تحدد أي تطبيق. تحديد التطبيقات الموصى بها؟
- تحديد التطبيقات الموصى بها؟
- وحدة Xposed لم يتم تفعيلها بعد
- مُوصى به
- تحديث متاح: %1$s
- الوحدة %s تم تعطيلها لعدم تحديد أي تطبيق.
- إطار النظام
- نسخ احتياطي
- نسخ احتياطي
- استعادة
- إيقاف إجباري
- إيقاف إجباري؟
- إذا أغلقت التطبيق إجبارياً، قد يتصرف بشكل خاطئ.
- إعادة التشغيل مطلوبة لتطبيق هذا التغيير
- إعادة تشغيل
- إخفاء
-
- عرض في تطبيق آخر
- معلومات التطبيق
- ¯\\\\_(ツ)_\/¯\n لا شيء هنا
-
- إطار العمل
- تعطيل السجلات المفصّلة
- الإبلاغ عن مشاكل طلب لتضمين السجلات المفصولة
- السمة السوداء المظلمة
- استخدام السمة السوداء الخالصة إذا تم تمكين السمة المظلمة
- السمة
- النسخ الاحتياطي والاستعادة
- نسخ احتياطي لقائمة الوحدات وقوائم النطاق.
- استعادة قائمة الوحدات وقوائم النطاق.
- نسخ احتياطي
- فشل في النسخ الاحتياطي:\n%s
- الرجاء تمكين DocumentUI
- استعادة
- فشل في الاستعادة:\n%s
- شبكة
- DNS عبر HTTPS
- حل بديل لتسمم DNS في بعض الدول
- لون السمة
- لون سمة النظام
- إجبار التطبيقات على إظهار أيقونات المشغل
- بعد أندرويد 10، لا يسمح للتطبيقات بإخفاء أيقونات المشغل. قم بإيقاف تشغيل التبديل لتعطيل مِيزة النظام هذه.
- نظام
- اللغة
- المساهمون بالترجمة
- المشاركة في الترجمة
- ساعدنا في ترجمة %s إلى لغتك
- إنشاء اختصار يمكنه فتح مدير الطفيليات
- تم تثبيت الاختصار
- المشغل الافتراضي الحالي لا يدعم اختصارات الدبوس
- إشعارات الحالة
- إظهار إشعار يمكنه فتح مدير الطفيليات
- قناة التحديث
- مستقر
- تجريبي
- البناء الليلي
-
- اقرأني
- إصدارات
- معلومات
- الصفحة الرئيسية
- كود المصدر
- المتعاونين
- الأصول
- فتح في المتصفح
- إظهار الإصدارات الأقدم
- لا مزيد من الإصدار
- فشل تحميل مستودع الوحدة: %s
- قابل للترقية أولاً
- المثبتة
-
- - %d التنزيلات
- - %d تنزيل
- - %d التنزيلات
- - %d التنزيلات
- - %d التنزيلات
- - %d التنزيلات
-
-
- لون ساكورا
- أحمر
- وردي
- بنفسجي
- بنفسجي عميق
- نيلي
- أزرق
- أزرق فاتح
- سماوي
- أزرق مخضرّ
- أخضر
- أخضر فاتح
- ليموني
- أصفر
- كهرماني
- برتقالي
- برتقالي عميق
- بني
- أزرق رمادي
-
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
deleted file mode 100644
index 935aba7bf..000000000
--- a/app/src/main/res/values-bg/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- نظرة عامة
- الوحدات
-
- - %d модулът е активиран
- - %d включени модули
-
- Дневници
- Настройки
- Обратна връзка или предложение
- За нас
- Докладване на проблем
- Хранилище
- Всички модули са актуализирани
- Публикувано в %s
- Акттуализиран на %s
-
- - %d модула има актуализация
- - %d модули с актуализации
-
- Присъединете се към нашия %2$s канал]]>
- невалидно
- Инсталиране на
- Натиснете, за да инсталирате LSPosed
- Не е инсталиран
- LSPosed не е инсталиран
- Активиран
- Частично активиран
- SEPolicy не е заредена правилно
- Моля, докладвайте за това на Magisk разработчик.]]>
- Неуспешно инжектиране на системната рамка
- Magisk или някои нискокачествени модули на Magisk. Моля, опитайте се да деактивирате модулите на Magisk, различни от Riru и LSPosed, или изпратете пълен журнал на разработчиците.]]>
- Неправилна стойност на системата
- Понякога модулите могат да се обезсилват.]]>
- Необходимо е да се актуализира
- Моля, инсталирайте най-новата версия на LSPosed
- Версия на API
- Версия на рамката
- Име на пакета на мениджъра
- Версия на системата
- Устройство
- ABI на системата
- Обвивка на Dex Optimizer
- Разрешено
- Не е разрешено
- Поддържан
- Неподдържан
- Версията за Android е неудовлетворена
- Счупен
- Монтирането е неуспешно
- SELinux е разрешаващ
- Политиката на SELinux е неправилна
- Актуализиране на LSPosed
- Потвърждаване на актуализацията на LSPosed? Това устройство ще се рестартира след завършване на актуализацията
- Копиране в клипборда
-
- Добре дошли в LSPosed
- Използвате паразитния мениджър, който може да създаде пряк път или все още да се отваря от известието.
- Използвате паразитния мениджър, който може да се отвори от известие.
- Създаване на пряк път
- Никога не показвайте
- Препоръчва се паразитен мениджър
- LSPosed вече поддържа паразитиране на системата, за да се избегне откриването, можете да отворите мениджъра на паразити от известието. Препоръчва се да деинсталирате текущото приложение.
-
- Запазете
- Условни дневници
- Дневници на модулите
- Запазване на дневника, моля изчакайте
- Запазени дневници
- Не успяхте да запазите:\n%s
- Изчистване на дневника сега
- Дневникът е изчистен успешно.
- Превъртете към началото
- Зареждане…
- Превъртете към дъното
- Презареждане
- Неуспешно изчистване на дневника
- Обвиване на думата
- Разрешен е вербален дневник
- Деактивиран е вербалният дневник
-
- (не е предоставено описание)
- Този модул изисква по-нова версия на Xposed (%d) и поради това не може да бъде активиран
- Този модул е предназначен за по-нова версия на Xposed (%d) и поради това някои функционалности може да не работят
- Този модул не посочва версията на Xposed, която му е необходима.
- Този модул е създаден за Xposed версия %1$d, но поради несъвместими промени във версия %2$d, той е деактивиран
- Този модул не може да бъде зареден, защото е инсталиран на SD картата, моля, преместете го във вътрешната памет
- Деинсталиране на
- Настройки на модула
- Преглед в Repo
- Искате ли да деинсталирате този модул?
- Деинсталиран %1$s
- Деинсталирането е неуспешно
- Добавяне на модул към потребителя
- Добавяне на %1$s към потребител %2$s
- Добавянето на модул е неуспешно
- Инсталиране на потребител %s
- Искате да инсталирате %1$s на потребител %2$s? Препоръчително е да се инсталира ръчно, принудителното инсталиране чрез LSPosed може да доведе до проблеми.
- разширяване на
- срив
-
- Оптимизиране на
- Оптимизиране на…
- Оптимизацията е завършена
- Стартирайте го
- Оптимизацията е неуспешна: върнатата стойност е празна
- Оптимизацията е неуспешна:
- Име на приложението
- Име на пакета
- Време за инсталиране
- Време за актуализация
- Обратен
- Системни приложения
- Сортиране
- Включване на модула
- Не сте избрали нито едно приложение. Продължете?
- Игри
- Модули
- Неуспешно запазване на списъка с обхвата
- Версия: %1$s
- Препоръчителен
- Не сте избрали нито едно приложение. Избрахте препоръчани приложения?
- Изберете препоръчани приложения?
- Модулът Xposed все още не е активиран
- Препоръчителен
- Налична е актуализация: %1$s
- Модулът %s е деактивиран, тъй като не е избрано приложение.
- Рамка на системата
- Резервно копие
- Резервно копие
- Възстановяване на
- Спиране на силата
- Насилствено спиране?
- Ако спрете приложение принудително, то може да се държи неправилно.
- Необходимо е рестартиране, за да се приложи тази промяна.
- Рестартиране на
- Скрий
-
- Преглед в друго приложение
- Информация за приложението
- ¯\\\\_(ツ)_\/¯\nНищо тук
-
- Рамка
- Деактивиране на вербалните дневници
- Искане за включване на вербални дневници
- Черна тъмна тема
- Използвайте чисто черната тема, ако е активирана тъмна тема
- Тема
- Архивиране и възстановяване
- Списък с резервни копия на модули и списъци на обхвата.
- Възстановяване на списъците с модули и области.
- Резервно копие
- Неуспешно архивиране:\n%s
- Моля, разрешете DocumentUI
- Възстановяване на
- Неуспешно възстановяване:\n%s
- Мрежа
- DNS през HTTPS
- Заобикаляне на отравянето на DNS в някои държави
- Цвят на темата
- Цвят на темата на системата
- Принуждаване на приложенията да показват икони на стартирането
- След Android 10 на приложенията не е позволено да скриват иконите си за стартиране. Изключете превключвателя, за да деактивирате тази системна функция.
- Система
- Език
- Преводачи, които допринасят за превода
- Участие в превод
- Помогнете ни да преведем %s на вашия език
- Създаване на пряк път, който може да отваря паразитен мениджър
- Кратък път, закачен
- Текущият стартер по подразбиране не поддържа преки пътища
- Известие за състоянието
- Показване на известие, което може да отвори паразитен мениджър
- Актуализиране на канала
- Стабилен
- Бета
- Нощно изграждане
-
- Readme
- Освобождава
- Информация
- Начална страница
- Изходен код
- Сътрудници
- Активи
- Отваряне в браузъра
- Показване на по-стари версии
- Няма повече освобождаване
- Неуспешно зареждане на модул repo: %s
- Може да се надгражда първо
- Инсталиран
-
- - %d изтегляне
- - %d Изтегляния
-
-
- Сакура
- Червено
- Розов
- Лилаво
- Наситено лилаво
- Indigo
- Синьо
- Светлосиньо
- Cyan
- Teal
- Зелен
- Светлозелено
- Lime
- Жълт
- Амбър
- Orange
- Наситено оранжево
- Кафяв
- Синьо сиво
-
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
deleted file mode 100644
index 5a14afb1b..000000000
--- a/app/src/main/res/values-bn/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- সার সংক্ষেপ
- মডিউল
-
- - %d মডিউল সক্ষম
- - %d মডিউল সক্রিয়
-
- তথ্য সার-সংক্ষেপ
- বিন্যাস
- প্রতিক্রিয়া বা পরামর্শ
- সম্পর্কিত
- সমস্যা প্রতিবেদন
- ভান্ডার
- সব মডিউল আপ টু ডেট
- %sএ প্রকাশিত
- %sএ আপডেট করা হয়েছে
-
- - %d মডিউল আপগ্রেডযোগ্য
- - %d মডিউল আপগ্রেডযোগ্য
-
- এ সোর্স কোড দেখুন আমাদের %2$s চ্যানেলে যোগ দিন]]>
- bdtipsntricks
- ইনস্টল করুন
- LSPosed ইনস্টল করতে আলতো চাপুন
- ইনস্টল করা না
- LSPosed ইনস্টল করা হয় না
- সক্রিয়
- আংশিক সক্রিয়
- এসইপলিসি সঠিকভাবে লোড করা হয় না
- অনুগ্রহ করে এটি Magisk বিকাশকারীকে রিপোর্ট করুন।]]>
- সিস্টেম ফ্রেমওয়ার্ক ইনজেকশন ব্যর্থ হয়েছে
- Magisk বা কিছু নিম্নমানের Magisk মডিউলের কারণে হতে পারে। অনুগ্রহ করে Riru এবং LSPosed ব্যতীত Magisk মডিউলগুলি নিষ্ক্রিয় করার চেষ্টা করুন বা বিকাশকারীদের কাছে সম্পূর্ণ লগ জমা দিন।]]>
- সিস্টেম প্রপ ভুল
- মডিউল মাঝে মাঝে অবৈধ হতে পারে।]]>
- আপডেট করতে হবে
- অনুগ্রহ করে LSPosed এর সর্বশেষ সংস্করণটি ইনস্টল করুন
- API সংস্করণ
- ফ্রেমওয়ার্ক সংস্করণ
- ম্যানেজার প্যাকেজের নাম
- সিস্টেম সংস্করণ
- যন্ত্র
- সিস্টেম ABI
- ডেক্স অপ্টিমাইজার মোড়ক
- সক্রিয়
- সক্রিয় না
- সমর্থিত
- অসমর্থিত
- অ্যান্ড্রয়েড সংস্করণ অসন্তুষ্ট
- বিধ্বস্ত
- মাউন্ট ব্যর্থ হয়েছে
- SELinux অনুমোদিত
- SELinux নীতি ভুল
- আপডেট LSPosed
- LSPosed আপডেট করার জন্য নিশ্চিত? আপডেট সম্পূর্ণ হওয়ার পরে এই ডিভাইসটি রিবুট হবে
- ক্লিপবোর্ডে কপি করা হয়েছে
-
- LSPosed স্বাগতম
- আপনি পরজীবী ম্যানেজার ব্যবহার করছেন, যা শর্টকাট তৈরি করতে পারে বা এখনও বিজ্ঞপ্তি থেকে খুলতে পারে।
- আপনি পরজীবী ম্যানেজার ব্যবহার করছেন, যা বিজ্ঞপ্তি থেকে খুলতে পারে।
- শর্টকাট তৈরি করুন
- কখনও দেখাবে না
- পরজীবী ব্যবস্থাপক প্রস্তাবিত
- LSPosed এখন সনাক্তকরণ এড়াতে সিস্টেম প্যারাসাইটাইজেশন সমর্থন করে, আপনি বিজ্ঞপ্তি থেকে পরজীবী ম্যানেজার খুলতে পারেন। বর্তমান অ্যাপ্লিকেশনটি আনইনস্টল করার পরামর্শ দেওয়া হচ্ছে।
-
- সংরক্ষণ
- ভার্বোস লগ
- মডিউল লগ
- লগ সংরক্ষণ করা হচ্ছে, অনুগ্রহ করে অপেক্ষা করুন
- লগ সংরক্ষিত
- সংরক্ষণ করতে ব্যর্থ হয়েছে:\n%s
- এখন লগ সাফ করুন
- লগ সফলভাবে সাফ করা হয়েছে৷
- উপরে যান
- লোড হচ্ছে…
- নীচে স্ক্রোল করুন
- পুনরায় লোড করুন
- লগ সাফ করতে ব্যর্থ হয়েছে
- শব্দ মোড়ানো
- ভার্বোস লগ সক্ষম
- ভার্বোস লগ নিষ্ক্রিয়
-
- (কোন বর্ণনা দেওয়া হয়নি)
- এই মডিউলটির একটি নতুন Xposed সংস্করণ প্রয়োজন (%d) এবং এইভাবে সক্রিয় করা যাবে না
- এই মডিউলটি একটি নতুন Xposed সংস্করণ (%d) এর জন্য ডিজাইন করা হয়েছে এবং এইভাবে কিছু কার্যকারিতা কাজ নাও করতে পারে
- এই মডিউলটি তার প্রয়োজনীয় Xposed সংস্করণটি নির্দিষ্ট করে না।
- এই মডিউলটি Xposed সংস্করণ %1$d-এর জন্য তৈরি করা হয়েছিল, কিন্তু সংস্করণ %2$d-এ অসামঞ্জস্যপূর্ণ পরিবর্তনের কারণে, এটি নিষ্ক্রিয় করা হয়েছে।
- এই মডিউলটি লোড করা যাবে না কারণ এটি SD কার্ডে ইনস্টল করা আছে, দয়া করে এটিকে অভ্যন্তরীণ সঞ্চয়স্থানে নিয়ে যান৷
- আনইনস্টল করুন
- মডিউল সেটিংস
- রেপোতে দেখুন
- আপনি এই মডিউল আনইনস্টল করতে চান?
- আনইনস্টল %1$s
- আনইনস্টল করা যায়নি
- ব্যবহারকারীর জন্য মডিউল যোগ করুন
- ব্যবহারকারী %2$sএ %1$s যোগ করা হয়েছে
- মডিউল যোগ করা ব্যর্থ হয়েছে৷
- ব্যবহারকারী %sএ ইনস্টল করুন
- ব্যবহারকারী %2$sথেকে %1$s ইনস্টল করতে চান? এটি ম্যানুয়ালি ইনস্টল করার সুপারিশ করা হয়, LSPosed এর মাধ্যমে জোর করে ইনস্টল করার ফলে সমস্যা হতে পারে।
- বিস্তৃত করা
- পতন
-
- পুনরায় অপ্টিমাইজ করুন
- অপ্টিমাইজ করা…
- অপ্টিমাইজেশান সম্পূর্ণ
- এটি চালু করুন
- অপ্টিমাইজেশান ব্যর্থ হয়েছে: রিটার্ন মান খালি
- অপ্টিমাইজেশান ব্যর্থ হয়েছে:
- আবেদনের নাম
- প্যাকেজের নাম
- ইন্সটল করার সময়
- আপডেটের সময়
- বিপরীত
- সিস্টেম অ্যাপস
- শ্রেণীবিভাজন
- মডিউল সক্ষম করুন
- আপনি কোনো অ্যাপ নির্বাচন করেননি। চালিয়ে যান?
- গেমস
- মডিউল
- সুযোগ তালিকা সংরক্ষণ করতে ব্যর্থ হয়েছে
- সংস্করণ: %1$s
- প্রস্তাবিত
- আপনি কোনো অ্যাপ নির্বাচন করেননি। প্রস্তাবিত অ্যাপ নির্বাচন করবেন?
- প্রস্তাবিত অ্যাপ নির্বাচন করবেন?
- Xposed মডিউল এখনও সক্রিয় করা হয় নি
- প্রস্তাবিত
- আপডেট উপলব্ধ: %1$s
- মডিউল %s অক্ষম করা হয়েছে যেহেতু কোনো অ্যাপ নির্বাচন করা হয়নি৷
- সিস্টেম ফ্রেমওয়ার্ক
- ব্যাকআপ
- ব্যাকআপ
- পুনরুদ্ধার করুন
- জোরপুর্বক থামা
- জোরপুর্বক থামা?
- আপনি যদি একটি অ্যাপকে জোর করে বন্ধ করেন, তাহলে সেটি খারাপ আচরণ করতে পারে।
- এই পরিবর্তনটি প্রয়োগ করার জন্য রিবুট প্রয়োজন
- রিবুট করুন
- লুকান
-
- অন্য অ্যাপে দেখুন
- অ্যাপের তথ্য
- ¯\\\\_(ツ)_\/¯\nএখানে কিছুই নেই
-
- ফ্রেমওয়ার্ক
- ভার্বোস লগ অক্ষম করুন
- ভার্বোস লগগুলি অন্তর্ভুক্ত করার জন্য সমস্যার প্রতিবেদন করুন
- কালো অন্ধকার থিম
- অন্ধকার থিম সক্ষম থাকলে খাঁটি কালো থিম ব্যবহার করুন
- থিম
- ব্যাকআপ এবং পুনঃস্থাপন
- ব্যাকআপ মডিউল তালিকা এবং সুযোগ তালিকা.
- মডিউল তালিকা এবং সুযোগ তালিকা পুনরুদ্ধার করুন।
- ব্যাকআপ
- ব্যাকআপ করতে ব্যর্থ হয়েছে:\n%s
- অনুগ্রহ করে ডকুমেন্টইউআই সক্ষম করুন
- পুনরুদ্ধার করুন
- পুনরুদ্ধার করতে ব্যর্থ হয়েছে:\n%s
- অন্তর্জাল
- HTTPS এর উপর DNS
- কিছু দেশে ডিএনএস বিষক্রিয়ার সমাধান
- থিম রঙ
- সিস্টেম থিম রঙ
- অ্যাপ্লিকেশানগুলিকে লঞ্চার আইকনগুলি দেখাতে বাধ্য করুন৷
- অ্যান্ড্রয়েড 10 এর পরে, অ্যাপগুলিকে তাদের লঞ্চার আইকনগুলি লুকানোর অনুমতি দেওয়া হয় না। এই সিস্টেম বৈশিষ্ট্যটি নিষ্ক্রিয় করতে টগলটি বন্ধ করুন৷
- পদ্ধতি
- ভাষা
- অনুবাদ অবদানকারী
- অনুবাদে অংশগ্রহণ করুন
- আপনার ভাষায় %s অনুবাদ করতে আমাদের সাহায্য করুন
- একটি শর্টকাট তৈরি করুন যা পরজীবী ম্যানেজার খুলতে পারে
- শর্টকাট পিন করা হয়েছে
- বর্তমান ডিফল্ট লঞ্চার পিন শর্টকাট সমর্থন করে না
- স্থিতি বিজ্ঞপ্তি
- পরজীবী ম্যানেজার খুলতে পারে এমন একটি বিজ্ঞপ্তি দেখান
- চ্যানেল আপডেট করুন
- স্থিতিশীল
- বেটা
- রাতারাতি নির্মাণ
-
- রিডমি
- মুক্তি দেয়
- তথ্য
- হোমপেজ
- সোর্স কোড
- সহযোগীরা
- সম্পদ
- ব্রাউজারে খোলা
- পুরোনো সংস্করণ দেখান
- আর মুক্তি নেই
- মডিউল রেপো লোড করতে ব্যর্থ হয়েছে: %s
- প্রথমে আপগ্রেডযোগ্য
- ইনস্টল করা হয়েছে
-
- - %d ডাউনলোড
- - %d ডাউনলোড
-
-
- সাকুরা
- লাল
- গোলাপী
- বেগুনি
- গভীর বেগুনি
- নীল
- নীল
- হালকা নীল
- সায়ান
- টিল
- সবুজ
- হালকা সবুজ
- চুন
- হলুদ
- অ্যাম্বার
- কমলা
- গভীর কমলা
- বাদামী
- নীল ধূসর
-
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
deleted file mode 100644
index c49d65383..000000000
--- a/app/src/main/res/values-ca/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Visió general
- Mòduls
-
- - %d Mòdul activat
- - %d Mòduls activats
-
- Logs
- Configuració
- Comentari o suggeriment
- Sobre
- Reportar problema
- Repositori
- Tots els mòduls actualitzats
- Published at %s
- Actualitzat a les %s
-
- - %d mòdul actualitzable
- - %d mòduls actualitzables
-
- Uneix-te al nostre %2$s canal]]>
- Frederic Blay, Ghost Face, Yannick Kamin
- Instalar
- Toca per instal·lar LSPosed
- No instalat
- LSPosed no està instal·lat
- Activat
- Parcialment activat
- SEPolicy no s\'ha carregat correctament
- Informeu-ho al desenvolupador Magisk .]]>
- La injecció del marc del sistema ha fallat
- Magisk o algún Mòdul de baixa qualitat Si us plau, prova a desactivar els demés mòduls de magisk excepte Riru i LSPosed o envia un informe complet als desarrolladors.]]>
- Prop del sistema incorrecte
- Els mòduls poden invalidar-se ocasionalment.]]>
- Cal actualitzar
- Instal·leu la darrera versió de LSPosed
- Versió de l\'API
- Versió del marc
- Nom del paquet del gestor
- Versió del sistema
- Dispositiu
- Sistema ABI
- Embolcall de Dex Optimizer
- Habilitat
- No habilitat
- Admet
- Sense suport
- La versió d\'Android no està satisfeta
- Estavellat
- El muntatge ha fallat
- SELinux és permissiu
- La política de SELinux és incorrecta
- Actualització LSPosed
- Confirmeu per actualitzar LSPosed? Aquest dispositiu es reiniciarà un cop finalitzada l\'actualització
- S\'ha copiat al porta-retalls
-
- Benvingut a LSPosed
- Esteu utilitzant el gestor de paràsits, que pot crear dreceres o encara obrir-se des de la notificació.
- Esteu utilitzant el gestor de paràsits, que es pot obrir des de la notificació.
- Crear accès directe
- No mostris mai
- Administrador de paràsits recomanat
- LSPosed ara admet la parasitització del sistema per evitar la detecció, podeu obrir el gestor de paràsits des de la notificació. Es recomana desinstal·lar l\'aplicació actual.
-
- Desa
- Registres detallats
- Registres de mòduls
- S\'està desant el registre, espereu
- Registres guardats
- No s\'ha pogut desar:\n%s
- Esborra el registre ara
- El registre s\'ha esborrat correctament.
- Desplaceu-vos cap a dalt
- Carregant…
- Desplaceu-vos cap avall
- Recarregar
- No s\'ha pogut esborrar el registre
- L\'ajust de línia
- Registre detallat activat
- Registre detallat desactivat
-
- (no s\'ofereix cap descripció)
- Aquest mòdul requereix una versió més nova de Xposed (%d) i per tant no es pot activar
- Aquest mòdul està dissenyat per a una versió més nova de Xposed (%d) i per tant algunes funcionalitats poden no funcionar
- Aquest mòdul no especifica la versió de Xposed que necessita.
- Aquest mòdul es va crear per a Xposed versió %1$d, però a causa de canvis incompatibles a la versió %2$d, s\'ha desactivat
- Aquest mòdul no es pot carregar perquè està instal·lat a la targeta SD, moveu-lo a l\'emmagatzematge intern
- Desinstal·la
- Configuració del mòdul
- Veure a Repo
- Voleu desinstal·lar aquest mòdul?
- Desinstal·lat %1$s
- La desinstal·lació no s\'ha realitzat correctament
- Afegeix un mòdul a l\'usuari
- S\'ha afegit %1$s a l\'usuari %2$s
- S\'ha produït un error en afegir el mòdul
- Instal·lar a l\'usuari %s
- Voleu instal·lar %1$s a l\'usuari %2$s? Es recomana instal·lar manualment, forçar la instal·lació mitjançant LSPosed pot causar problemes.
- expandir
- col·lapse
-
- Torna a optimitzar
- Optimització…
- Optimització completa
- Llança\'l
- L\'optimització ha fallat: el valor de retorn és buit
- L\'optimització ha fallat:
- Nom de l\'aplicació
- Nom del paquet
- Temps d\'instal·lació
- Hora d\'actualització
- Revés
- Aplicacions del sistema
- Classificació
- Activa el mòdul
- No heu seleccionat cap aplicació. Continuar?
- Jocs
- Mòduls
- No s\'ha pogut desar la llista d\'àmbits
- Versió: %1$s
- Recomanat
- No heu seleccionat cap aplicació. Seleccioneu aplicacions recomanades?
- Vols seleccionar aplicacions recomanades?
- El mòdul Xposed encara no està activat
- Recomanat
- Actualització disponible: %1$s
- El mòdul %s s\'ha desactivat perquè no s\'ha seleccionat cap aplicació.
- Marc del sistema
- Còpia de seguretat
- Còpia de seguretat
- Restaurar
- Parada forçada
- Parada forçada?
- Si forços l\'aturada d\'una aplicació, és possible que es comporti malament.
- Cal reiniciar perquè s\'apliqui aquest canvi
- Reinicieu
- Amaga
-
- Veure en una altra aplicació
- Informació de l\'aplicació
- ¯\\\\_(ツ)_\/¯\nAquí no hi ha res
-
- Marc
- Desactiva els registres detallats
- Sol·licitud d\'informes de problemes per incloure registres detallats
- Tema negre fosc
- Utilitzeu el tema negre pur si el tema fosc està habilitat
- Tema
- Còpia de seguretat i restaurar
- Llista de mòduls de còpia de seguretat i llistes d\'abast.
- Restaura la llista de mòduls i les llistes d\'abast.
- Còpia de seguretat
- No s\'ha pogut fer la còpia de seguretat:\n%s
- Si us plau, activeu DocumentUI
- Restaurar
- No s\'ha pogut restaurar:\n%s
- Xarxa
- DNS sobre HTTPS
- Solució alternativa a l\'enverinament per DNS en algunes nacions
- Color del tema
- Color del tema del sistema
- Força les aplicacions a mostrar icones del llançador
- Després d\'Android 10, les aplicacions no poden amagar les icones del llançador. Desactiveu el commutador per desactivar aquesta funció del sistema.
- Sistema
- Llenguatge
- Col·laboradors de traducció
- Participar en la traducció
- Ajuda\'ns a traduir %s al teu idioma
- Creeu una drecera que pugui obrir el gestor de paràsits
- Drecera fixada
- El llançador predeterminat actual no admet dreceres de pin
- Notificació d\'estat
- Mostra una notificació que pugui obrir el gestor de paràsits
- Actualitza el canal
- Estable
- Beta
- Construcció nocturna
-
- Llegiu-me
- Alliberaments
- Informació
- Pàgina d\'inici
- Codi font
- Col·laboradors
- Actius
- Oberta al navegador
- Mostra les versions anteriors
- No més llançament
- No s\'ha pogut carregar el dipòsit del mòdul: %s
- Actualitzable primer
- Instal·lat
-
- - %d descàrrega
- - %d descàrregues
-
-
- Sakura
- Vermell
- Rosa
- Porpra
- Lila fosc
- Indigo
- Blau
- Blau clar
- Cian
- Teal
- verd
- Verd clar
- Lima
- groc
- Ambre
- taronja
- Taronja profund
- marró
- Gris blau
-
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
deleted file mode 100644
index 009db0cf1..000000000
--- a/app/src/main/res/values-cs/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- Přehled
- Moduly
-
- - %d modul aktivován
- - %d modul povolen
- - %d Modul aktivován
- - %d Modul aktivován
-
- Protokoly
- Nastavení
- Zpětná vazba nebo návrh
- O aplikaci
- Nahlásit problém
- Repozitář
- Všechny moduly jsou aktuální
- Publikováno v %s
- Aktualizováno v %s
-
- - %d modul je možné aktualizovat
- - %d moduly je možné aktualizovat
- - %d modolů je možné aktualizovat
- - %d modulů je možné aktualizovat
-
- Připojte se k našemu kanálu %2$s]]>
- https://www.instagram.com/kasi33/
- Instalovat
- Klepnutím nainstalujete LSPosed
- Není nainstalováno
- LSPosed není nainstalován
- Aktivováno
- Částečně aktivováno
- SEPolicy není správně načten
- Nahlaste to prosím vývojáři Magisk .]]>
- Načtení System Framework se nezdařilo
- Magiskem nebo některými málo kvalitními moduly Magisku. Zkuste vypnout moduly Magisk jiné než Riru a LSPosed nebo odešlete kompletní log vývojářům.]]>
- Nesprávné systémové prop
- Moduly mohou být někdy neplatné a tedy nefunkční.]]>
- Je třeba aktualizovat
- Nainstalujte si prosím nejnovější verzi LSPosed
- Verze API
- Verze frameworku
- Název balíčku správce
- Verze systému
- Zařízení
- Systémové ABI (architektura)
- Dex Optimizer Wrapper
- Povoleno
- Není povoleno
- Podporováno
- Nepodporováno
- Verze Androidu není spokojena
- Havaroval
- Připojení se nezdařilo
- SELinux je permisivní
- Zásady SELinuxu jsou nesprávné
- Aktualizovat LSPosed
- Potvrdit aktualizaci LSPosed? Toto zařízení se restartuje po dokončení aktualizace
- Zkopírováno do schránky
-
- Vítejte v LSPosed
- Používáte parazitního správce, který může vytvořit zástupce nebo se stále otevírat z oznámení.
- Používáte parazitního správce, který se může otevřít z oznámení.
- Vytvořit zástupce
- Nikdy nezobrazovat
- Doporučený parazitický manažer
- LSPosed nyní podporuje parazitování systému. K zabránění detekce můžete správce parazitů otevřít z oznámení. Doporučuje se odinstalovat aktuální aplikaci.
-
- Uložit
- Podrobné protokoly
- Protokoly modulů
- Ukládání protokolu, čekejte prosím
- Protokoly uloženy
- Nepodařilo se uložit:\n%s
- Vymazat log
- Log byl úspěšně vymazán.
- Přejít na začátek
- Načítání…
- Přejít na začátek
- Znovu načíst
- Nepodařilo se vymazat protokol
- Zalamování řádků
- Podrobný záznam povolen
- Podrobný záznam zakázán
-
- (žádný popis)
- Tento modul vyžaduje novější Xposed verzi (%d) a proto nemůže být aktivován
- Tento modul je určen pro novější verzi Xposed (%d), a proto některé funkce nemusí fungovat
- Tento modul nespecifikuje potřebnou Xposed verzi.
- Tento modul byl vytvořen pro Xposed verzi %1$d, a tak z důvodu nekompatibilních změn ve verzi %2$dbyl zakázán
- Tento modul nelze načíst, protože je nainstalován na SD kartě, přesuňte jej na interní úložiště
- Odinstalovat
- Nastavení modulů
- Zobrazit v repozitáři
- Chcete odinstalovat tento modul?
- %1$s odinstalován
- Odinstalace nebyla úspěšná
- Přidat modul k uživateli
- Modul %1$s přidán k uživateli %2$s
- Přidání modulu se nezdařilo
- Instalovat uživateli %s
- Chcete nainstalovat %1$s uživateli %2$s.? Je doporučeno instalovat ručně, vynucení instalace přes LSPosed může způsobit problémy.
- rozbalit
- sbalit
-
- Znovu optimalizovat
- Optimalizace…
- Optimalizace dokončena
- Spustit
- Optimalizace selhala: návratová hodnota je prázdná
- Optimalizace selhala:
- Název aplikace
- Název balíčku
- Doba instalace
- Čas aktualizace
- Obrátit pořadí řazení
- Systémové aplikace
- Řazení
- Povolit modul
- Nevybrali jste žádnou aplikaci. Pokračovat?
- Hry
- Moduly
- Nepodařilo se uložit seznam
- Verze: %1$s
- Zvolit doporučené
- Nevybrali jste žádnou aplikaci. Vybrat doporučené aplikace?
- Vybrat doporučené aplikace?
- Xposed modul ještě není aktivován
- Doporučené
- Je k dispozici aktualizace: %1$s
- Modul %s byl zakázán, protože nebyla vybrána žádná aplikace.
- Systémový Framework
- Zálohování
- Zálohovat
- Obnovení
- Vynutit zastavení
- Vynutit zastavení?
- Pokud vynutíte zastavení aplikace, může dojít k chybnému chování.
- Pro aplikaci této změny je vyžadován restart
- Restartovat
- Skrýt
-
- Zobrazit v jiné aplikaci
- Informace o aplikaci
- <unk> \\\\_(<unk> )_\/ <unk>\nTady nic není
-
- Framework
- Zakázat podrobné protokoly
- Nahlásit požadavek na problémy a zahrnout detailní záznamy
- Čistě černý motiv
- Použít čistý černý motiv, pokud je tmavý motiv povolen
- Vzhled
- Zálohování a obnovení
- Zálohovat seznam modulů a nastavení.
- Obnovit seznam modulů a nastavení.
- Zálohování
- Zálohování se nezdařilo:\n%s
- Prosím povolte DocumentUI
- Obnovení
- Nepodařilo se obnovit:\n%s
- Síť
- DNS over HTTPS
- Řešení DNS oprav v některých zemích
- Barva motivu
- Barva systémového motivu
- Vynutit aplikace k zobrazení ikon spouštěče
- Od Androidu 10 není aplikacím povoleno skrývat ikony spouštěče. Vypněte přepínač pro vypnutí této systémové funkce.
- Systém
- Jazyk
- Přispěvatelé překladu
- Účast na překladu
- Pomozte nám přeložit %s do vašeho jazyka
- Vytvoření zástupce, který může otevřít parazitního správce
- Připnutí zástupci
- Současný výchozí spouštěč nepodporuje připnuté zkratky
- Oznámení o stavu
- Zobrazení oznámení, které může otevřít parazitního správce
- Kanál aktualizace
- Stabilní
- Beta
- Noční sestavení
-
- Přečti si mě
- Vydání
- Informace
- Domovská stránka
- Zdrojový kód
- Spolupracovníci
- Assets
- Otevřít v prohlížeči
- Zobrazit starší verze
- Žádné další vydání
- Nepodařilo se načíst repozitář modulu: %s
- Nejprve aktualizovatelný
- Instalováno
-
- - %d stažení
- - %d ke stažení
- - %d ke stažení
- - %d ke stažení
-
-
- Sakura
- Červená
- Růžová
- Fialová
- Tmavě fialová
- Indigo
- Modrá
- Světle modrá
- Azurová
- Modrozelená
- Zelená
- Světle zelená
- Limetková
- Žlutá
- Jantarová
- Oranžová
- Tmavě oranžová
- Hnědá
- Modro-šedivá
-
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
deleted file mode 100644
index acda6c34d..000000000
--- a/app/src/main/res/values-da/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Oversigt
- Moduler
-
- - %d modul aktiveret
- - %d moduler aktiveret
-
- Logfiler
- Indstillinger
- Feedback eller forslag
- Om
- Anmeld problem
- Lagre
- Alle moduler opdateret
- Udgivet på %s
- Opdateret på %s
-
- - %d modul opgraderbar
- - %d moduler opgraderbare
-
- Tilmeld dig vores %2$s kanal]]>
- null
- Installér
- Tryk for at installere LSPosed
- Ikke installeret
- LSPosed er ikke installeret
- Aktiveret
- Delvist aktiveret
- SEPolicy er ikke indlæst korrekt
- Du bedes rapportere dette til Magisk udvikleren.]]>
- System Framework injektion mislykkedes
- Magisk eller nogle lavkvalitets Magisk moduler. Prøv at deaktivere andre Magisk moduler end Riru og LSPosed eller indsende fuld log til udviklere.]]>
- System prop forkert
- Moduler kan ugyldiggøre lejlighedsvis.]]>
- Skal opdateres
- Installér venligst den seneste version af LSPosed
- API version
- Rammer version
- Navn på manager-pakke
- System version
- Enhed
- System ABI
- Dex Optimizer Wrapper
- Aktiveret
- Ikke aktiveret
- Understøttet
- Ikke understøttet
- Android-version utilfreds
- Nedstyrtet
- Montering mislykkedes
- SELinux er tilladende
- SELinux-politik er forkert
- Opdater LSPosed
- Bekræft opdatering af LSPosed? Denne enhed vil genstarte efter opdateringsfuldførelse
- Kopieret til udklipsholderen
-
- Velkommen til LSPosed
- Du bruger den parasitære manager, som kan oprette genvej eller stadig åbne fra meddelelsen.
- Du bruger den parasitære manager, som kan åbnes fra notifikationen.
- Opret genvej
- Vis aldrig
- Parasitic Manager Anbefalet
- LSPosed understøtter nu systemparasitering for at undgå registrering, du kan åbne parasitmanager fra meddelelsen. Det anbefales at afinstallere det aktuelle program.
-
- Gem
- Verbose Logs
- Moduler Logs
- Gemmer log, vent venligst
- Gemte logfiler
- Mislykkedes at gemme:\n%s
- Ryd log nu
- Loggen blev ryddet.
- Rul til toppen
- Indlæser…
- Rul til bunden
- Reload
- Kunne ikke rydde loggen
- Tekstombrydning
- Verbose log aktiveret
- Verbose log deaktiveret
-
- (ingen beskrivelse angivet)
- Dette modul kræver en nyere Xposed version (%d) og kan derfor ikke aktiveres
- Dette modul er designet til en nyere Xposed-version (%d), og derfor fungerer nogle funktioner muligvis ikke
- Dette modul angiver ikke den Xposed version det behøver.
- Dette modul blev oprettet til Xposed version %1$d, men på grund af inkompatible ændringer i version %2$d, er det blevet deaktiveret
- Dette modul kan ikke indlæses, da det er installeret på SD-kortet, flyt det til intern lagerplads
- Afinstaller
- Modul indstillinger
- Se i Repo
- Vil du afinstallere dette modul?
- Afinstalleret %1$s
- Afinstallation mislykkedes
- Tilføj modul til bruger
- Tilføjede %1$s til bruger %2$s
- Tilføjelse af modul mislykkedes
- Installér til bruger %s
- Vil du installere %1$s til bruger %2$s? Det anbefales at installere manuelt, tvinger installation via LSPosed kan forårsage problemer.
- udvid
- kollaps
-
- Genoptimér
- Optimerer…
- Optimering fuldført
- Start det
- Optimering mislykkedes: returværdi er tom
- Optimering mislykkedes:
- Applikations navn
- Pakke navn
- Installér tid
- Opdater tid
- Omvendt
- System apps
- Sortering
- Aktiver modul
- Du valgte ikke nogen app. Fortsæt?
- Spil
- Moduler
- Kunne ikke gemme scope-liste
- Version: %1$s
- Anbefalet
- Du valgte ikke nogen app. Vælg anbefalede apps?
- Vælg anbefalede apps?
- Xposed modul er endnu ikke aktiveret
- Anbefalet
- Opdatering tilgængelig: %1$s
- Modul %s er blevet deaktiveret siden ingen app er valgt.
- System Framework
- Sikkerhedskopi
- Sikkerhedskopi
- Gendan
- Gennemtving stop
- Gennemtving stop?
- Hvis du tvinger til at stoppe en app, kan den virke forkert.
- Genstart er påkrævet for at denne ændring kan anvendes
- Reboot
- Skjul
-
- Se i anden app
- Oplysninger om appen
- Spredning \\\\_(Ι)_\/ Ι\nIntet her
-
- Framework
- Deaktivere udførlige logfiler
- Anmodning om at medtage verbose logs i rapporten om problemer
- Sort mørkt tema
- Brug det rene sorte tema, hvis mørkt tema er aktiveret
- Tema
- Sikkerhedskopiér og gendan
- Backup modul liste og scope-lister.
- Gendan modulliste og scope-lister.
- Sikkerhedskopi
- Sikkerhedskopiering mislykkedes:\n%s
- Aktiver venligst DocumentUI
- Gendan
- Kunne ikke gendanne:\n%s
- Netværk
- DNS over HTTPS
- Workaround DNS forgiftning i nogle nationer
- Tema farve
- Farve på systemtema
- Tving apps til at vise launcher-ikoner
- Efter Android 10 må apps ikke skjule deres launcher-ikoner. Slå toggle fra for at deaktivere denne systemfunktion.
- System
- Sprog
- Oversættelsesbidragsydere
- Deltag i oversættelse
- Hjælp os med at oversætte %s til dit sprog
- Opret en genvej, der kan åbne parasitic manager
- Genvej fastgjort
- Den nuværende standardstarter understøtter ikke pin-genveje
- Meddelelse om status
- Vis en meddelelse, der kan åbne parasitic manager
- Opdater kanal
- Stabil
- Beta
- Nightly build
-
- Læs
- Udgivelser
- Info
- Hjemmeside
- Kilde kode
- Samarbejdspartnere
- Aktiver
- Åbn i browser
- Vis ældre versioner
- Ikke mere udgivelse
- Kunne ikke indlæse modul repo: %s
- Opgradérbar først
- Installeret
-
- - %d download
- - %d downloads
-
-
- Sakura
- Rød
- Lyserød
- Lilla
- Dyb lilla
- Indigo
- Blå
- Lyseblå
- Cyan
- Grønblåt
- Grøn
- Lysegrøn
- Limegrøn
- Gul
- Ravgul
- Orange
- Dyb orange
- Brun
- Blå grå
-
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
deleted file mode 100644
index 4214f8caa..000000000
--- a/app/src/main/res/values-de/strings.xml
+++ /dev/null
@@ -1,243 +0,0 @@
-
-
-
-
- Übersicht
- Module
-
- - %d Modul aktiviert
- - %d Module aktiviert
-
- Protokolle
- Einstellungen
- Feedback oder Vorschlag
- Über
- Fehler melden
- Modularchiv
- Alle Module sind auf dem neusten Stand
- Veröffentlicht am %s
- Aktualisiert am %s
-
- - %d Modul Update verfügbar
- - %d Modul Updates verfügbar
-
- Folge unserem %2$s-Kanal]]>
- mshinni80,
-JJ108
- Installieren
- Tippen um LSPosed zu installieren
- Nicht installiert
- LSPosed ist nicht installiert
- Aktiviert
- Teilweise aktiviert
- SEPolicy wird nicht korrekt geladen
- Bitte melde es dem Magisk Entwickler.]]>
- System-Framework-Injektion fehlgeschlagen
- Magisk oder einige minderwertige Magisk Module verursacht werden. Bitte deaktiviere alle Magisk-Module bis auf Riru und LSPosed, oder sende eine vollständige Fehlermeldung an die Entwickler.]]>
- System-Prop falsch
- Module können gelegentlich außer Kraft gesetzt werden.]]>
- Aktualisierung erforderlich
- Bitte installieren Sie die neueste Version von LSPosed
- Tipps für Modulentwickler
- Bitte deaktiviere Deploy-Optimierungen in Android Studio oder benutze den `gradlew installDebug` Befehl zum Installieren. Andernfalls wird die Modul-Apk nicht aktualisiert.
- API Version
- Framework Version
- Name des Managerpakets
- System Version
- Gerät
- System ABI
- Dex Optimizer Wrapper
- Aktiviert
- Deaktiviert
- Unterstützt
- Nicht unterstützt
- Android-Version passt nicht
- Abgestürzt
- Mounten fehlgeschlagen
- SELinux ist permissiv
- SELinux-Richtlinie ist falsch
- Bitte LSPosed aktualisieren
- Soll LSPosed aktualisiert werden? Dieses Gerät wird nach dem Update neu gestartet
- In die Zwischenablage kopiert
-
- Willkommen bei LSPosed
- Du verwendest den parasitären Manager, der eine Verknüpfung erstellen oder über eine Benachrichtigung noch geöffnet werden kann.
- Du verwendest den parasitären Manager, der von der Benachrichtigung geöffnet werden kann.
- Verknüpfung erstellen
- Niemals anzeigen
- Parasitärer Manager empfohlen
- LSPosed unterstützt nun Systemparasitisierung, um eine Erkennung zu vermeiden. Du kannst den parasitären Manager über die Benachrichtigung öffnen. Es wird empfohlen, die aktuelle App zu deinstallieren.
-
- Speichern
- Ausführliche Protokolle
- Modul-Protokolle
- Protokoll wird gespeichert, bitte warten
- Gespeicherte Protokolle
- Speichern fehlgeschlagen:\n%s
- Protokoll jetzt löschen
- Protokoll erfolgreich gelöscht.
- Hochscrollen
- Laden…
- Runterscrollen
- Erneut laden
- Protokoll löschen fehlgeschlagen
- Wortumbruch
- Ausführliches Protokoll aktiviert
- Ausführliches Protokoll deaktiviert
-
- (keine Beschreibung angegeben)
- Dieses Modul erfordert eine neuere Version von LSPosed (%d) und kann daher nicht aktiviert werden
- Dieses Modul wurde für eine neuere Xposed-Version (%d) entwickelt und daher funktionieren einige Funktionen möglicherweise nicht
- Dieses Modul gibt nicht die benötigte LSPosed-Version an.
- Dieses Modul wurde für die LSPosed-Version %1$d erstellt, wurde jedoch aufgrund inkompatibler Änderungen in der Version %2$d deaktiviert
- Dieses Modul kann nicht geladen werden, da es auf der SD-Karte installiert ist, bitte in den internen Speicher verschieben
- Deinstallieren
- Moduleinstellungen
- In Repo anzeigen
- Möchtest du dieses Modul deinstallieren?
- %1$s deinstalliert
- Deinstallation fehlgeschlagen
- Modul zum Benutzer hinzufügen
- %1$s zu Benutzer %2$s hinzugefügt
- Modul hinzufügen fehlgeschlagen
- Auf Benutzer %s installieren
- Möchtest du %1$s auf Benutzer %2$s installieren? Es wird empfohlen manuell zu installieren, das Erzwingen der Installation über LSPosed kann Probleme verursachen.
- ausklappen
- einklappen
-
- Erneut optimieren
- Optimieren …
- Optimierung abgeschlossen.
- Starten
- Optimierung fehlgeschlagen: Rückgabewert ist leer
- Optimierung fehlgeschlagen:
- App-Name
- Paketname
- Installationszeit
- Aktualisierungszeit
- Umkehren
- System-Apps
- Sortieren
- Modul aktivieren
- Du hast keine App ausgewählt. Weiter?
- Spiele
- Module
- Scope-Liste speichern fehlgeschlagen
- Version: %1$s
- Auswählen
- Empfohlen
- Du hast keine App ausgewählt. Empfohlene Apps auswählen?
- Empfohlene Apps auswählen?
- Alle Auswählen
- Keine Auswahl
- Automatisch einbinden
- Das LSPosed-Modul wurde noch nicht aktiviert
- Empfohlen
- Aktualisierung verfügbar: %1$s
- Modul %s wurde deaktiviert, da keine App ausgewählt wurde.
- System-Framework
- Sichern
- Sichern
- Wiederherstellen
- Stopp erzwingen
- Stopp erzwingen?
- Wenn du einen App-Stopp erzwingst, können Probleme entstehen.
- Neustart erforderlich, um diese Änderung zu übernehmen
- Neustart
- Ausblenden
-
- In anderer App anzeigen
- App-Information
- ¯\\\\_(ツ)_\/¯\nNichts hier
-
- Framework
- Ausführliche Protokolle deaktivieren
- Ausführliche Protokolle in Problemberichtsmeldungen einschließen
- Dunkelschwarzes Thema
- Schwarzes Thema verwenden, wenn dunkles Thema aktiviert ist
- Design
- Sichern und Wiederherstellen
- Modul- und Scope-Listen sichern.
- Modul- und Scope-Listen wiederherstellen
- Sichern
- Sicherung fehlgeschlagen:\n%s
- Bitte DocumentUI aktivieren
- Wiederherstellen
- Wiederherstellung fehlgeschlagen:\n%s
- Netzwerk
- DNS über HTTPS
- Problemumgehung für DNS-Vergiftungen in einigen Ländern
- Designfarbe
- System Themenfarbe
- Apps erzwingen Launcher-Symbole anzuzeigen
- Ab Android 10 dürfen Apps ihre Launcher-Symbole nicht ausblenden. Schalte den Schalter aus, um diese System-Funktion zu deaktivieren.
- System
- Sprache
- Übersetzer
- Beim Übersetzen mitmachen
- Helfe uns, %s in deine Sprache zu übersetzen
- Eine Verknüpfung zum Öffnen des parasitären Managers erstellen
- Verknüpfung angeheftet
- Der aktuelle Standard-Launcher unterstützt keine Pin-Verknüpfungen
- Status-Benachrichtigung
- Eine Benachrichtigung anzeigen, die den parasitären Manager öffnen kann
- Update-Kanal
- Stabil
- Beta
- Nightly Build
-
- Liesmich
- Veröffentlichungen
- Info
- Webseite
- Quellcode
- Mitarbeiter
- Ressourcen
- Im Browser öffnen
- Ältere Versionen anzeigen
- Keine Veröffentlichung mehr
- Modularchiv laden fehlgeschlagen: %s
- Aktualisierbare zuerst
- Eingerichtet
-
- - %d Download
- - %d Downloads
-
-
- Sakura
- Rot
- Pink
- Lila
- Dunkellila
- Indigoblau
- Blau
- Hellblau
- Türkis
- Blaugrün
- Grün
- Hellgrün
- Limette
- Gelb
- Bernstein
- Orange
- Dunkelorange
- Braun
- Blaugrau
-
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
deleted file mode 100644
index 3f04c57b2..000000000
--- a/app/src/main/res/values-el/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Επισκόπηση
- Πρόσθετα
-
- - %d πρόσθετο ενεργοποιημένο
- - %d ενθέματα ενεργοποιήθηκαν
-
- Αρχεία καταγραφής
- Ρυθμίσεις
- Σχόλια ή πρόταση
- Πληροφορίες
- Αναφορά προβλήματος
- Αποθετήριο
- Όλα τα πρόσθετα είναι ενημερωμένα
- Δημοσιεύθηκε στο %s
- Ενημερώθηκε στο %s
-
- - %d ένθεμα αναβαθμίσιμο
- - %d πρόσθετα μπορούν να ενημερωθούν
-
- Εγγραφείτε στο %2$s κανάλι μας]]>
- Aristeidis Alexopoulos
- Εγκατάσταση
- Πατήστε για εγκατάσταση του LSPosed
- Μη εγκατεστημένο
- Το LSPosed δεν είναι εγκατεστημένο
- Ενεργοποιημένο
- Μερικώς ενεργοποιημένο
- Το SEPolicy δεν φορτώθηκε σωστά
- Παρακαλούμε αναφέρετε το γεγονός αυτό στον προγραμματιστή Magisk .]]>
- Η έγχυση στο πλαίσιο συστήματος απέτυχε
- Magisk ή από κάποια χαμηλής ποιότητας πρόσθετα τουMagisk. Παρακαλώ προσπαθήστε να απενεργοποιήσετε όλα τα πρόσθετα του Magisk εκτός από το Riru και το LSPosed ή να υποβάλετε το πλήρες αρχείο καταγραφής στους προγραμματιστές.]]>
- Λανθασμένο στήριγμα συστήματος
- Τα πρόσθετα μπορεί να ακυρωθούν περιστασιακά.]]>
- Απαιτείται ενημέρωση
- Παρακαλώ εγκαταστήστε την τελευταία έκδοση του LSPosed
- Έκδοση API
- Έκδοση πλαισίου
- Όνομα πακέτου διαχειριστή
- Έκδοση συστήματος
- Συσκευή
- Σύστημα ABI
- Dex Optimizer Wrapper
- Ενεργό
- Μη ενεργό
- Υποστηριζόμενο
- Μη υποστηριζόμενο
- Ανικανοποίητη έκδοση Android
- Συνετρίβη
- Mount απέτυχε
- Το SELinux είναι επιτρεπτικό
- Η πολιτική SELinux είναι λανθασμένη
- Ενημέρωση LSPosed
- Επιβεβαίωση ενημέρωσης του LSPosed? Αυτή η συσκευή θα επανεκκινηθεί μετά την ολοκλήρωση της ενημέρωσης
- Αντιγραφή στο πρόχειρο
-
- Καλωσορίσατε στο LSPosed
- Χρησιμοποιείτε τον παρασιτικό διαχειριστή, ο οποίος μπορεί να δημιουργήσει συντόμευση ή να παραμένει ανοικτός από την ειδοποίηση.
- Χρησιμοποιείτε τον παρασιτικό διαχειριστή, ο οποίος μπορεί να δημιουργήσει συντόμευση ή να παραμένει ανοικτός από την ειδοποίηση.
- Δημιουργία συντόμευσης
- Να μην εμφανίζεται ποτέ
- Προτεινόμενος Παρασιτικός Διαχειριστής
- Το LSPosed υποστηρίζει τώρα την παρασιτοποίηση του συστήματος για να αποφύγετε την ανίχνευση, μπορείτε να ανοίξετε τον παρασιτικό διαχειριστή από την ειδοποίηση. Συνιστάται να απεγκαταστήσετε την τρέχουσα εφαρμογή.
-
- Αποθήκευση
- Λεπτομερείς Καταγραφές
- Αρχείο Καταγραφής Πρόσθετων
- Αποθήκευση αρχείου καταγραφής, παρακαλώ περιμένετε
- Αποθηκευμένα αρχεία καταγραφής
- Αποτυχία αποθήκευσης:\n%s
- Καθαρισμός αρχείου καταγραφής τώρα
- Το αρχείο καταγραφής εκκαθαρίστηκε επιτυχώς.
- Κύλιση στην κορυφή
- Φόρτωση…
- Κύλιση προς τα κάτω
- Φόρτωση ξανά
- Αποτυχία εκκαθάρισης του αρχείου καταγραφής
- Αναδίπλωση Λέξεων
- Ενεργοποίηση λεπτομερούς καταγραφής
- Λεπτομερής καταγραφή απενεργοποιημένη
-
- (δεν παρέχεται περιγραφή)
- Αυτό το ένθεμα απαιτεί μια νεότερη έκδοση του Xposed (%d) και επομένως δεν μπορεί να ενεργοποιηθεί
- Αυτή η ενότητα έχει σχεδιαστεί για μια νεότερη έκδοση Xposed (%d) και ως εκ τούτου ορισμένες λειτουργίες ενδέχεται να μην λειτουργούν.
- Αυτό το ένθεμα δεν καθορίζει την έκδοση Xposed που χρειάζεται.
- Αυτό το ένθεμα δημιουργήθηκε για την έκδοση Xposed %1$d, αλλά λόγω μη συμβατών αλλαγών στην έκδοση %2$d, έχει απενεργοποιηθεί
- Αυτό το πρόσθετο δεν μπορεί να φορτωθεί επειδή είναι εγκατεστημένο στην κάρτα SD, παρακαλώ μετακινήστε το στον εσωτερικό αποθηκευτικό χώρο
- Απεγκατάσταση
- Ρυθμίσεις πρόσθετου
- Προβολή στο Repo
- Θέλετε να απεγκαταστήσετε αυτό το πρόσθετο?
- Απεγκαταστάθηκε %1$s
- Απεγκατάσταση ανεπιτυχής
- Προσθήκη module στο χρήστη
- Προστέθηκε %1$s στον χρήστη %2$s
- Η προσθήκη module απέτυχε
- Εγκατάσταση σε χρήστη %s
- Θέλετε να εγκαταστήσετε %1$s στο χρήστη %2$s? Συνιστάται η χειροκίνητη εγκατάσταση, αναγκάζοντας την εγκατάσταση μέσω LSPosed μπορεί να προκαλέσει προβλήματα.
- επέκταση
- σύμπτυξη
-
- Επαναβελτιστοποίηση
- Βελτιστοποίηση…
- Η βελτιστοποίηση ολοκληρώθηκε
- Εκκίνηση
- Η βελτιστοποίηση απέτυχε: η τιμή επιστροφής είναι κενή
- Η βελτιστοποίηση απέτυχε:
- Όνομα εφαρμογής
- Όνομα πακέτου
- Χρόνος εγκατάστασης
- Χρόνος ενημέρωσης
- Αντίστροφη
- Εφαρμογές συστήματος
- Ταξινόμηση
- Ενεργοποίηση module
- Δεν έχετε επιλέξει καμία εφαρμογή. Συνέχεια?
- Παιχνίδια
- Πρόσθετα
- Αποτυχία αποθήκευσης της λίστας πεδίου
- Έκδοση: %1$s
- Προτεινόμενο
- Δεν έχετε επιλέξει καμία εφαρμογή. Επιλέξτε τις προτεινόμενες εφαρμογές?
- Επιλέξτε προτεινόμενες εφαρμογές?
- Το Xposed πρόσθετο δεν έχει ενεργοποιηθεί ακόμα
- Προτεινόμενο
- Διαθέσιμη ενημέρωση: %1$s
- Το ένθεμα %s έχει απενεργοποιηθεί δεδομένου ότι δεν έχει επιλεγεί εφαρμογή.
- Πλαίσιο Συστήματος
- Αντίγραφα Ασφαλείας
- Αντίγραφα Ασφαλείας
- Επαναφορά
- Αναγκαστική διακοπή
- Αναγκαστική διακοπή?
- Αν επιβάλετε τη διακοπή μιας εφαρμογής, ενδέχεται να μην λειτουργήσει σωστά.
- Απαιτείται επανεκκίνηση για να εφαρμοστεί αυτή η αλλαγή
- Reboot
- Απόκρυψη
-
- Προβολή σε άλλη εφαρμογή
- Πληροφορίες εφαρμογής
- ◆ \\\\_(\")_\/ \"\nΤίποτα εδώ
-
- Framework
- Απενεργοποιήστε αναλυτικά στοιχεία καταγραφής
- Αναφορά προβλημάτων ζητάει να συμπεριλαμβάνεται τα αναλυτικά στοιχεία καταγραφής
- Μαύρο σκούρο θέμα
- Χρησιμοποιήστε το καθαρό μαύρο θέμα αν το σκούρο θέμα είναι ενεργοποιημένο
- Θέμα
- Αντίγραφα ασφαλείας και επαναφορά
- Λίστα module αντιγράφων ασφαλείας και λίστες εμβέλειας.
- Επαναφορά λίστας ενθεμάτων και πεδίου εφαρμογής.
- Αντίγραφα Ασφαλείας
- Αποτυχία δημιουργίας αντιγράφων ασφαλείας:\n%s
- Ενεργοποιήστε το DocumentUI
- Επαναφορά
- Αποτυχία επαναφοράς:\n%s
- Δίκτυο
- DNS μέσω HTTPS
- Εργαστείτε γύρω από τη δηλητηρίαση DNS σε ορισμένες χώρες
- Χρώμα θέματος
- Χρώμα θέματος συστήματος
- Εξαναγκασμός των εφαρμογών να εμφανίζουν εικονίδια εκκίνησης
- Μετά το Android 10, οι εφαρμογές δεν επιτρέπεται να αποκρύψουν τα εικονίδια εκτοξευτή τους. Απενεργοποιήστε την εναλλαγή για να απενεργοποιήσετε αυτήν τη λειτουργία συστήματος.
- Σύστημα
- Γλώσσα
- Συντελεστές μετάφρασης
- Συμμετοχή στη μετάφραση
- Βοηθήστε μας να μεταφράσουμε το %s στη γλώσσα σας
- Δημιουργήστε μια συντόμευση που μπορεί να ανοίξει τον παρασιτικό διαχειριστή
- Συντόμευση καρφιτσωμένη
- Ο τρέχων προεπιλεγμένος εκτοξευτής δεν υποστηρίζει συντομεύσεις καρφίτσας
- Ειδοποίηση καταστάσεως
- Εμφάνιση μιας ειδοποίησης που μπορεί να ανοίξει τον διαχειριστή παρασιτικής
- Ενημέρωση καναλιού
- Σταθερό
- Βήτα
- Νυχτερινή κατασκευή
-
- Έτοιμο
- Εκδόσεις
- Πληροφορίες
- Αρχική
- Πηγαίος κώδικας
- Συνεργάτες
- Ενεργητικό
- Άνοιγμα σε πρόγραμμα περιήγησης
- Εμφάνιση παλαιότερων εκδόσεων
- Δεν υπάρχει πλέον έκδοση
- Αποτυχία φόρτωσης πρόσθετου repo: %s
- Αναβαθμίσιμο πρώτα
- Εγκατεστημένο
-
- - %d λήψη
- - %d downloads
-
-
- Sakura
- Κόκκινο
- Ροζ
- Μωβ
- Βαθύ μωβ
- Indigo
- Μπλε
- Ανοιχτό μπλε
- Κυανό
- Τιρκουάζ
- Πράσινο
- Ανοιχτό πράσινο
- Άσβεστος
- Κίτρινο
- Κεχριμπάρι
- Πορτοκαλί
- Βαθύ πορτοκαλί
- Καφέ
- Μπλε γκρι
-
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
deleted file mode 100644
index 3e0991f7f..000000000
--- a/app/src/main/res/values-es/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- Resumen
- Módulos
-
- - %d módulo activado
- - %d Módulos activados
-
- Trozas
- Configuración
- Comentarios o sugerencia
- Acerca de
- Reportar problema
- Repositorio
- Todos los modulos actualizados
- Publicado el %s
- Actualizado el %s
-
- - %d módulo actualizable
- - %d módulos repositorio actualizables
-
- Únete a nuestro %2$s canal]]>
- squaredDot
- Instalar
- Pulsa para instalar LSPosed
- No instalado
- LSPosed no está instalado
- Activado
- Parcialmente activado
- Selinux policy no está cargado correctamente
- Informa de ello al desarrollador de Magisk .]]>
- System Framework injection failed
- Magisk or some low-quality Magisk modules. Please try to disable Magisk modules other than Riru and LSPosed or submit full log to developers.]]>
- System prop incorrect
- Modules may invalidate occasionally.]]>
- Need to update
- Por favor instale la última versión de LSPosed
- Sugerencias para desarrolladores de módulos
- Por favor, desactiva las optimizaciones de implementación en Android Studio, o ejecuta el comando `gradlew installDebug` para instalar el módulo. De lo contrario, el apk del módulo no se actualizará.
- Versión de la API
- Versión del framework
- Nombre del paquete de gestión
- Versión del sistema
- Dispositivo
- ABI del Sistema
- Envoltura del optimizador Dex
- Activado
- No habilitado
- Apoyado
- No soportado
- Versión Android insatisfecha
- Se estrelló
- El montaje falló
- SELinux es permisivo
- La política de SELinux es incorrecta
- Actualizar LSPosed
- ¿Confirmar para actualizar LSPose? Este dispositivo se reiniciará después de completar la actualización
- Información copiada al portapapeles
-
- Bienvenido a LSPosed
- Usted está utilizando el gestor de parásitos, que puede crear acceso directo o todavía abierta de notificación.
- Estás usando el gestor de parásitos, que se puede abrir desde la notificación.
- Crear acceso directo
- Nunca mostrar
- Se recomienda Parasitic Manager
- LSPosed ahora soporta la parasitación del sistema para evitar la detección, puede abrir el gestor de parásitos desde la notificación. Se recomienda desinstalar la aplicación actual.
-
- Guardar
- Registros detallados
- Logs de los módulos
- Guardando registro, por favor espere
- Registros guardados
- No se pudo guardar:\n%s
- Limpiar los registros
- Registros limpiados satisfactoriamente.
- Desplazar hasta el inicio
- Cargando…
- Desplazar hasta el final
- Recargar
- Fallo al limpiar los registros
- Ajuste de palabras
- Registro detallado habilitado
- Registro detallado desactivado
-
- (sin descripción)
- Este módulo requiere una versión más nueva de Xposed (%d), por lo que no puede ser activado
- Este módulo está diseñado para una versión más reciente Xposed (%d) y por lo tanto algunas funcionalidades pueden no funcionar
- Este módulo no especifica la versión de Xposed que necesita.
- Este módulo fue creado para la versión de Xposed %1$d, pero, debido a cambios incompatibles en la versión %2$d, ha sido desactivado
- Este módulo no puede ser cargado porque está instalado en la tarjeta SD. Por favor, muévelo al almacenamiento interno
- Desinstalar
- Configuración del módulo
- Ver en el repositorio
- ¿Quieres desinstalar este módulo?
- Desinstalado %1$s
- Fallo en la desinstalación
- Añadir módulo al usuario
- %1$s instalado %2$s
- Fallo en la instalación
- Instalar al usuario %s
- ¿Quieres instalar %1$s al usuario %2$s? Se recomienda que lo instales manualmente; forzar la instalación a través de LSPosed puede causar problemas.
- expandir
- contraer
-
- Optimizar de nuevo
- Optimizando…
- Optimización completada.
- Abrir
- La optimización falló o devolvió un valor vacío.
- Fallo en la optimización:
- Filtrar por nombre de aplicación
- Filtrar por nombre de paquete
- Filtrar por fecha de instalación
- Filtrar por fecha de actualización
- Invertir
- Aplicaciones del sistema
- Filtrando
- Activar módulo
- No seleccionaste ninguna aplicación. ¿Quieres continuar?
- Juegos
- Módulos
- Fallo al guardar la lista de scopes
- Versión: %1$s
- Seleccionar
- Recomendado
- No seleccionaste ninguna aplicación. ¿Quieres seleccionar las aplicaciones recomendadas?
- ¿Quieres seleccionar las aplicaciones recomendadas?
- Todo
- Ninguno
- Auto-Incluir
- El módulo Xposed no está activado aún
- Recomendado
- Actualización disponible: %1$s
- El módulo %s ha sido desactivado ya que no se ha seleccionado ninguna aplicación.
- Framework del sistema
- Respaldo
- Hacer un respaldo
- Restaurar
- Forzar la detención
- ¿Quieres forzar la detención?
- Si fuerzas la detención de una aplicación puede que esta se comporte de manera indefinida.
- Necesitas reiniciar la aplicación para aplicar este cambio
- Reiniciar
- Ocultar
-
- Ver en otra aplicación
- Información de la aplicación
- ¯\\\\_(ツ)_\/¯\nNo hay nada por aquí
-
- Framework
- Desactivar registros detallados
- Solicitud de inclusión de registros detallados en los informes de incidencias
- Tema negro oscuro
- Usar el tema negro puro si el tema oscuro está activado
- Tema
- Respaldo y restauración
- Hacer un respaldo de la lista de módulos y scopes.
- Hacer una restauración de la lista de módulos y scopes.
- Hacer un respaldo
- Error al realizar la copia de seguridad:\n%s
- Por favor, activa DocumentUI
- Restaurar
- Error al restaurar:\n%s
- Red
- DNS sobre HTTPS
- Solución alternativa al ataque de DNS en algunos países
- Color del tema
- Color de acentuación del sistema
- Forzar a las aplicaciones a mostrar los íconos del ejecutable
- En versiones posteriores a Android 10 no se permite a las aplicaciones (especialmente los módulos de Xposed) a ocultar el logo de su ejecutable. Desactiva la opción para desactivar esta característica.
- Sistema
- Idioma
- Colaboradores de traducción
- Participar en la traducción
- Ayúdanos a traducir %s a tu idioma
- Crear un acceso directo que pueda abrir el gestor de parásitos
- Acceso directo anclado
- El actual lanzador por defecto no admite accesos directos a pines
- Notificación de estado
- Mostrar una notificación que puede abrir el gestor de parásitos
- Actualizar canal
- Estable
- Beta
- Construcción nocturna
-
- Léeme
- Versiones
- Información
- Página principal
- Código fuente
- Colaboradores
- Archivos
- Abrir en el navegador
- Mostrar versiones anteriores
- No hay más versiones
- Fallo al cargar el módulo de repositorio: %s
- Actualizables
- Instalado
-
- - %d descargar
- - %d descargas
-
-
- Sakura
- Rojo
- Rosa
- Morado
- Morado profundo
- Indigo
- Azul
- Azul claro
- Cian
- Teal
- Verde
- Verde claro
- Lima
- Amarillo
- Ámbar
- Naranja
- Naranja oscuro
- Marrón
- Gris azul
-
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
deleted file mode 100644
index b07528766..000000000
--- a/app/src/main/res/values-et/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Kodu
- Moodulid
-
- - %d moodul on lubatud
- - %d moodulit on lubatud
-
- Logid
- Seaded
- Tagasiside või ettepanek
- Kohta
- Teata probleemist
- Repo
- Kõik moodulid on ajakohased
- Avaldatud %s
- Uuendatud %s
-
- - %d moodul on täiendatav
- - %d moodulit täiendatavad
-
- Liitu meie %2$s \'i kanaliga]]>
- Subaru Pan
- Installi
- Puudutage LSPosedi installimiseks
- Puudub
- LSPosed ei ole installitud
- Aktiveeritud
- Osaliselt aktiveeritud
- SEPolicy ei ole korralikult laetud
- Palun teatage sellest Magisk arendajale.]]>
- System Frameworki süstimine ebaõnnestus
- Magisk või mõned madala kvaliteediga Magisk-moodulid. Palun proovige lülitada välja Magisk\'i moodulid peale Riru ja LSPosed või esitage täielik logi arendajatele.]]>
- Süsteemi tugi vale
- Moodulid võivad aeg-ajalt kehtetuks muutuda.]]>
- Vaja uuendada
- Palun installige LSPosedi uusim versioon
- API\'i versioon
- Raamistiku versioon
- Manager paketi nimi
- Süsteemi versioon
- Seade
- Süsteemi ABI
- Dex Optimizer Wrapper
- Lubatud
- Pole lubatud
- Toetatud
- Toetamata
- Androidi versioon ei ole toetatud
- Kokku jooksnud
- Kinnitus ebaõnnestus
- SELinux on lubav
- SELinux policy on vale
- LSPosedi uuendamine
- Kas kinnitada LSPosed uuendamine? See seade taaskäivitub pärast uuendamise lõpetamist
- Kopeeritud lõikelauale
-
- Tere tulemast LSPosedisse
- Sa kasutad parasiitide haldurit, mis võib luua otsetee või ikka avada teateid.
- Kasutate parasiitide haldurit, mida saab avada teatisest.
- Loo otsetee
- Mitte kunagi ei näidata
- Parasiitide haldur Soovitatav
- LSPosed toetab nüüd süsteemi parasiitide tuvastamise vältimiseks, saate avada parasiitide haldaja teatest. Praegune rakendus on soovitatav eemaldada.
-
- Salvesta
- Põhjalikud logid
- Moodulite logid
- Logi salvestamine, palun oodake
- Logi salvestatud
- Salvestamine ebaõnnestus:\n%s
- Kustuta logi kohe
- Logi edukalt kustutatud.
- Kerige üles
- Laadimine…
- Kerige alla
- Laadi uuesti
- Logi tühjendamine ebaõnnestus
- Word Wrap
- Paljusõnaline logi on lubatud
- Paljusõnaline logi on välja lülitatud
-
- (kirjeldus puudub)
- See moodul nõuab uuemat Xposed versiooni (%d) ja seega ei saa seda aktiveerida.
- See moodul on mõeldud uuemale Xposedi versioonile (%d) ja seetõttu ei pruugi mõned funktsioonid töötada
- See moodul ei täpsusta Xposedi versiooni, mida ta vajab.
- See moodul loodi Xposedi versiooni %1$d jaoks, kuid versioonis %2$d tehtud ühildumatute muudatuste tõttu on see välja lülitatud
- Seda moodulit ei saa laadida, sest see on paigaldatud SD-kaardile, palun viige see sisemällu.
- Eemalda
- Mooduli seaded
- Vaata Repos
- Kas soovite selle mooduli eemaldada?
- Eemaldatud %1$s
- Eemaldamine ebaõnnestus
- Lisa kasutajale moodul
- Lisatud %1$s kasutajale %2$s
- Mooduli lisamine ebaõnnestus
- Installi kasutajale %s
- Tahad paigaldada %1$s kasutajale %2$s? Soovitatav on paigaldada käsitsi, LSPosed\'i kaudu sunniviisiline paigaldamine võib põhjustada probleeme.
- laienda
- kollaps
-
- Optimeeri uuesti
- Optimeerimine…
- Optimeeritud
- Ava
- Optimeerimine ebaõnnestus: tagastusväärtus on tühi
- Optimeerimine ebaõnnestus:
- Rakenduse nimi
- Paketi nimi
- Installimise aeg
- Uuendamise aeg
- Tagasipööra
- Süsteemirakendused
- Sortimisalus
- Luba moodul
- Te ei valinud ühtegi rakendust. Jätka?
- Mängud
- Moodulid
- Ei õnnestunud salvestada reguleerimisala nimekirja
- Versioon: %1$s
- Soovitatav
- Te ei valinud ühtegi rakendust. Valige soovitatud rakendused?
- Valige soovitatavad rakendused?
- Xposed moodul ei ole aktiveeritud
- Soovitatav
- Uuendus on saadaval: %1$s
- Moodul %s on välja lülitatud, kuna ühtegi rakendust ei ole valitud.
- Süsteemi raamistik
- Varukoopia
- Varukoopia
- Taasta
- Sundpeata
- Sundpeata?
- Kui te peatate rakenduse sunniviisiliselt, võib see halvasti käituda.
- Selle muudatuse kohaldamiseks on vajalik taaskäivitamine
- Taaskäivitus
- Peida
-
- Vaadake teises rakenduses
- Rakenduse teave
- ¯\\\\_(ツ)_\/¯\nSiin pole midagi.
-
- Raamistik
- Lülita sõnalised logid välja
- Aruande probleemid taotluse lisada sõnalogid
- Must tume teema
- Kasutage puhast musta teemat, kui tume teema on lubatud.
- Teema
- Varundamine ja taastamine
- Moodulite varukoopiate nimekiri ja ulatusloendid.
- Taastab moodulite loendi ja ulatusloendite loendi.
- Varukoopia
- Varundamine ebaõnnestus:\n%s
- Palun lubage DocumentUI
- Taasta
- Ei õnnestunud taastada:\n%s
- Võrk
- DNS üle HTTPS
- Workaround DNS mürgistus mõnedes riikides
- Teema värv
- Süsteemi teema värv
- Rakenduste sundimine käivitaja ikoonide kuvamiseks
- Pärast Android 10 ei ole rakendustel lubatud oma käivitaja ikoonid ära peita. Selle süsteemifunktsiooni väljalülitamiseks lülitage lüliti välja.
- Süsteem
- Keel
- Tõlkimise toetajad
- Osalege tõlkimises
- Aita meil tõlkida %s sinu keelde
- Loo otsetee, mis võib avada parasiitide halduri
- Otsetee kinnitatud
- Praegune vaikekäivitusprogramm ei toeta nööpnõelte otseteid
- Staatuse teatamine
- Kuva teatis, mis võib avada parasiitide halduri
- Uuenduskanal
- Stabiilne
- Beeta
- Nightly build
-
- Readme
- Väljaanded
- Teave
- Koduleht
- Lähtekood
- Koostööpartnerid
- Varad
- Ava brauseris
- Kuva vanemad versioonid
- Enam ei ole väljaannet
- Ebaõnnestus mooduli repo laadimine: %s
- Esimesena uuendatav
- Paigaldatud
-
- - allalaaditud on %d kord
- - allalaaditud on %d korda
-
-
- Sakura
- Punane
- Roosa
- Lilla
- Sügavlilla
- Indigo
- Sinine
- Helesinine
- Tsüaansinine
- Sinakasroheline
- Roheline
- Heleroheline
- Laimiroheline
- Kollane
- Amber
- Oranž
- Sügavoranž
- Pruun
- Sinine hall
-
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
deleted file mode 100644
index dcec2c809..000000000
--- a/app/src/main/res/values-fa/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- نمای کلی
- ماژولها
-
- - %d ماژول فعال
- - %d ماژول فعال
-
- لاگ ها
- تنظیمات
- بازخورد یا پیشنهاد
- درباره
- گزارش مشکل
- مخزن
- همه ماژول ها بروز هستند
- منتشر شده در %s
- بروزرسانی شده در %s
-
- - %d ماژول قابل بروزرسانی
- - %d ماژول قابل بروزرسانی
-
- عضو کانال %2$s شوید]]>
- null
- نصب
- برای نصب LSPosed لمس کنید
- نصب نشده
- LSPosed نصب نشده
- فعال شده
- نیمه فعال
- سیاست SELinux به درستی بارگذاری نشده
- لطفاً این موضوع را به توسعه دهنده Magisk گزارش دهید.]]>
- تزریق به چارچوب سیستم ناموفق بود
- Magisk یا برخی ماژول های بی کیفیت Magisk باشد. لطفاً ماژول های Magisk به جز Riru و LSPosed را غیرفعال کنید یا لاگ کامل را برای توسعه دهندگان بفرستید.]]>
- ویژگی های سیستم نادرست است
- ماژول ها ممکن است گاهی کار نکنند.]]>
- نیاز به بروزرسانی
- LSPosedLSPosed
- نکات برای توسعه دهنده ماژول
- لطفاً بهینهسازیهای استقرار را در اندروید استودیو خاموش کنید یا از دستور `gradlew installDebug` استفاده کنید. در غیر این صورت APK ماژول آپدیت نمیشود.
- نسخه API
- نسخه چارچوب
- نام پکیج مدیر
- نسخه سیستم
- دستگاه
- ABI سیستم
- قالب بهینهسازی Dex
- فعال
- غیرفعال
- پشتیبانی شده
- پشتیبانی نمی شود
- نسخه اندروید پشتیبانی نمی شود
- کرش کرد
- مونت ناموفق بود
- SELinux در حالت Permissive است
- سیاست SELinux نادرست است
- بروزرسانی LSPosed
- آیا بروزرسانی LSPosed را تأیید می کنید؟ بعد از پایان، دستگاه ری استارت می شود
- کپی شد
-
- به LSPosed خوش آمدید
- شما از مدیر Parasitic استفاده می کنید که می تواند شورتکات بسازد یا از نوتیفیکیشن باز شود.
- شما از مدیر Parasitic استفاده می کنید که فقط از نوتیفیکیشن باز می شود.
- ساخت شورتکات
- هرگز نمایش نده
- مدیر Parasitic توصیه شده
- LSPosed حالا از سیستم Parasitic پشتیبانی می کند تا شناسایی نشود، می توانید از نوتیفیکیشن مدیر Parasitic را باز کنید. بهتر است برنامه فعلی را حذف کنید.
-
- ذخیره
- لاگ های مفصل
- لاگ های ماژول
- در حال ذخیره لاگ، لطفاً صبر کنید
- لاگ ها ذخیره شدند
- ذخیره موفق نبود:\n%s
- همین الان لاگ ها را پاک کن
- لاگ ها با موفقیت پاک شدند.
- برگشت به بالا
- در حال بارگذاری…
- رفتن به پایین
- بارگذاری مجدد
- پاک کردن لاگ ناموفق بود
- شکستن خودکار خطوط
- لاگ مفصل فعال شد
- لاگ مفصل غیرفعال شد
-
- (توضیحی داده نشده)
- این ماژول نیاز به نسخه جدیدتر Xposed (%d) دارد و نمی تواند فعال شود
- این ماژول برای نسخه جدیدتر Xposed (%d) ساخته شده، پس ممکن است برخی امکانات کار نکنند
- این ماژول نسخه Xposed مورد نیازش را مشخص نکرده.
- این ماژول برای نسخه %1$d ساخته شده، اما به دلیل تغییرات ناسازگار در نسخه %2$d غیرفعال شده
- این ماژول نمی تواند بارگذاری شود چون روی کارت حافظه نصب شده، لطفاً به حافظه داخلی منتقل کنید
- حذف نصب
- تنظیمات ماژول
- مشاهده در مخزن
- می خواهید این ماژول را حذف کنید؟
- حذف شد %1$s
- حذف موفق نبود
- اضافه کردن ماژول به کاربر
- اضافه شد %1$s به کاربر %2$s
- اضافه کردن ماژول موفق نبود
- نصب برای کاربر %s
- می خواهید %1$s را برای کاربر %2$s نصب کنید؟ توصیه می شود دستی نصب کنید، نصب با LSPosed ممکن است مشکل ایجاد کند.
- باز کن
- ببند
-
- بهینهسازی مجدد
- در حال بهینهسازی…
- بهینهسازی تمام شد
- باز کن
- بهینهسازی شکست خورد: خروجی خالی است
- بهینهسازی شکست خورد:
- نام برنامه
- نام پکیج
- زمان نصب
- زمان بروزرسانی
- معکوس
- برنامه های سیستمی
- مرتبسازی
- فعال کردن ماژول
- برنامه ای انتخاب نکردی، ادامه میدی؟
- بازی ها
- ماژول ها
- ذخیره لیست ناموفق بود
- نسخه: %1$s
- انتخاب
- توصیه شده
- برنامه ای انتخاب نکردی. برنامه های توصیه شده را انتخاب کنم؟
- می خوای برنامه های توصیه شده رو انتخاب کنی؟
- همه
- هیچی
- شامل خودکار
- ماژول Xposed هنوز فعال نشده
- توصیه شده
- بروزرسانی موجود: %1$s
- ماژول %s به خاطر انتخاب نکردن برنامه غیرفعال شده.
- چارچوب سیستم
- پشتیبان گیری
- پشتیبان گیری
- بازیابی
- توقف اجباری
- توقف اجباری؟
- اگر برنامه را به زور متوقف کنی، ممکن است درست کار نکند.
- برای اعمال تغییر باید ری استارت کنی
- ری استارت
- مخفی کن
-
- مشاهده در برنامه دیگر
- اطلاعات برنامه
- ¯\_(ツ)_/¯\nاینجا چیزی نیست
-
- چارچوب
- غیرفعال کردن لاگ مفصل
- لاگ مفصل برای گزارش مشکل لازم است
- تم سیاه کامل
- اگر تم تاریک فعال است از تم کاملا سیاه استفاده کن
- تم
- پشتیبان گیری و بازیابی
- پشتیبان گیری از لیست ماژول ها و برنامه ها.
- بازیابی لیست ماژول ها و برنامه ها.
- پشتیبان گیری
- پشتیبان گیری ناموفق بود:\n%s
- لطفاً DocumentUI را فعال کنید
- بازیابی
- بازیابی ناموفق بود:\n%s
- شبکه
- DNS روی HTTPS
- حل مشکل مسمومیت DNS در بعضی کشورها
- رنگ تم
- رنگ تم سیستم
- نمایش آیکون های لانچر برنامه ها
- از اندروید ۱۰ به بعد، برنامه ها نمی توانند آیکون لانچر را مخفی کنند. این گزینه را خاموش کن تا این ویژگی غیرفعال شود.
- سیستم
- زبان
- مشارکت کنندگان ترجمه
- مشارکت در ترجمه
- کمک کن %s را به زبان خودت ترجمه کنیم
- شورتکاتی بساز که مدیر Parasitic را باز کند
- شورتکات پین شد
- لانچر پیش فرض فعلی شورتکات های پین شده را پشتیبانی نمی کند
- نمایش اعلان وضعیت
- نمایش اعلانی که مدیر Parasitic را باز کند
- کانال بروزرسانی
- پایدار
- بتا
- نسخه شبانه
-
- راهنما
- نسخه ها
- اطلاعات
- صفحه اصلی
- سورس کد
- همکاران
- دارایی ها
- باز کردن در مرورگر
- نمایش نسخه های قدیمی تر
- نسخه ای بیشتر نیست
- بارگذاری مخزن ماژول شکست خورد: %s
- اول ماژول های قابل بروزرسانی
- نصب شده
-
- - %d دانلود
- - %d دانلود
-
-
- ساکورا
- قرمز
- صورتی
- بنفش
- بنفش تیره
- نیلی
- آبی
- آبی روشن
- آبی فیروزه ای
- آبی خاکستری
- سبز
- سبز روشن
- لیمویی
- زرد
- کهربایی
- نارنجی
- نارنجی تیره
- قهوه ای
- آبی خاکستری
-
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
deleted file mode 100644
index a39c6a90d..000000000
--- a/app/src/main/res/values-fi/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Yleiskatsaus
- Moduulit
-
- - %d moduuli käytössä
- - %d moduulia käytössä
-
- Lokit
- Asetukset
- Palaute tai ehdotus
- Tietoja
- Ilmoita ongelmasta
- Versiovarasto
- Kaikki moduulit ajan tasalla
- Julkaistu osoitteessa %s
- Päivitetty osoitteessa %s
-
- - %d moduuli päivitettävissä
- - %d moduulia päivitettävissä
-
- Liity kanavaamme %2$s]]>
- null
- Asenna
- Napauta asentaaksesi LSPosed
- Ei asennettu
- LSPosed ei ole asennettu
- Aktivoitu
- Osittain aktivoitu
- SEPolicy ei ole ladattu oikein
- Ilmoita tästä Magisk kehittäjälle.]]>
- Järjestelmän kehysinjektointi epäonnistui
- Magisk tai joitakin heikkolaatuisia Magisk moduuleja. Yritä poistaa käytöstä muut Magisk moduulit kuin Riru ja LSPosed tai lähettää täysi loki kehittäjille.]]>
- Järjestelmän prop virheellinen
- Moduulit voivat mitätöidä satunnaisesti.]]>
- Täytyy päivittää
- Asenna LSPosedin uusin versio
- API versio
- Kehyksen versio
- Manager-paketin nimi
- Järjestelmän versio
- Laite
- Järjestelmä ABI
- Dex Optimizer Wrapper
- Käytössä
- Ei käytössä
- Tuettu
- Ei tuettu
- Android-versio tyytymätön
- Crashed
- Kiinnitys epäonnistui
- SELinux on salliva
- SELinux-käytäntö on virheellinen
- Päivitys LSPostettu
- Vahvista LSPost-päivitys? Tämä laite käynnistyy uudelleen päivityksen jälkeen
- Kopioitu leikepöydälle
-
- Tervetuloa LSPosed
- Käytät loishallintaohjelmaa, joka voi luoda pikakuvakkeen tai silti avata ilmoituksen.
- Käytät loishallintaohjelmaa, joka voidaan avata ilmoituksesta.
- Luo pikakuvake
- Älä näytä koskaan
- Parasitic Manager Suositellaan
- LSPosed tukee nyt järjestelmän loisimista havaitsemisen välttämiseksi, voit avata loishallinnan ilmoituksesta. On suositeltavaa poistaa nykyinen sovellus.
-
- Tallenna
- Verbose Lokit
- Moduulien Lokit
- Lokin tallentaminen, odota
- Tallennetut lokit
- Tallennus epäonnistui:\n%s
- Tyhjennä loki nyt
- Loki tyhjennetty.
- Vieritä ylös
- Ladataan…
- Siirry alareunaan
- Reload
- Lokin tyhjentäminen epäonnistui
- Sanan Rivitys
- Verbose loki käytössä
- Verbose loki pois käytöstä
-
- (ei kuvausta annettu)
- Tämä moduuli vaatii uudemman Xposed version (%d) eikä sitä näin ollen voi aktivoida
- Tämä moduuli on suunniteltu uudemmalle Xposed-versiolle (%d), joten jotkin toiminnot eivät välttämättä toimi.
- Tämä moduuli ei määrittele tarvitsemaansa Xposed versiota.
- Tämä moduuli on luotu Xposed versiolle %1$d, mutta koska versiossa %2$don tehty yhteensopimattomia muutoksia, se on poistettu käytöstä
- Tätä moduulia ei voi ladata, koska se on asennettu SD-kortille, siirrä se sisäiseen tallennustilaan
- Poista
- Moduulin asetukset
- Näytä repossa
- Haluatko poistaa tämän moduulin?
- Poista %1$s
- Poisto epäonnistui
- Lisää moduuli käyttäjälle
- Lisätty %1$s käyttäjälle %2$s
- Moduulin lisääminen epäonnistui
- Asenna käyttäjälle %s
- Haluatko asentaa %1$s käyttäjälle %2$s? On suositeltavaa asentaa manuaalisesti, pakottaa asennus LSPosedin kautta voi aiheuttaa ongelmia.
- laajenna
- pienennä
-
- Uudelleenoptimoi
- Optimoidaan…
- Optimointi valmis
- Käynnistä se
- Optimointi epäonnistui: palautusarvo on tyhjä
- Optimointi epäonnistui:
- Sovelluksen nimi
- Paketin nimi
- Asenna aika
- Päivityksen aika
- Käänteinen
- Järjestelmäsovellukset
- Lajittelu
- Ota moduuli käyttöön
- Et valinnut yhtään sovellusta. Jatketaanko?
- Pelit
- Moduulit
- Valmistelulistan tallentaminen epäonnistui
- Versio: %1$s
- Suositeltu
- Et valinnut yhtään sovellusta. Valitse suositellut sovellukset?
- Valitse suositellut sovellukset?
- Xposed moduuli ei ole vielä aktivoitu
- Suositeltu
- Päivitys saatavilla: %1$s
- Moduuli %s on poistettu käytöstä koska sovellusta ei ole valittu.
- Järjestelmän Puitteet
- Varmuuskopio
- Varmuuskopio
- Palauta
- Pakota lopetus
- Pakotetaanko lopetus?
- Jos pakotat sovelluksen pysähtymään, se saattaa käyttäytyä väärin.
- Uudelleenkäynnistys vaaditaan tämän muutoksen käyttöönottamiseksi
- Reboot
- Piilota
-
- Näytä toisessa sovelluksessa
- Sovelluksen tiedot
- ¶ \\\\_(konferenssissa)_\/ ¶\nEi mitään tässä
-
- Framework
- Sanallisten lokien poistaminen käytöstä
- Raportti pyytää sisällyttämään sanalliset lokit
- Musta tumma teema
- Käytä puhdas musta teema, jos tumma teema on käytössä
- Teema
- Varmuuskopioi ja palauta
- Varmuuskopioi moduulien listat ja sisällysluettelot.
- Palauta moduulien luettelo ja sisällysluettelot.
- Varmuuskopio
- Varmuuskopiointi epäonnistui:\n%s
- Ota DocumentUI käyttöön
- Palauta
- Palautus epäonnistui:\n%s
- Verkko
- DNS yli HTTPS
- Workaround DNS myrkytys joissakin kansoissa
- Teeman väri
- Järjestelmän teeman väri
- Pakota sovellukset näyttämään käynnistimen kuvakkeet
- Android 10:n jälkeen sovellukset eivät saa piilottaa niiden käynnistyskuvakkeita. Poista valinta käytöstä poistaaksesi järjestelmän ominaisuuden.
- Järjestelmä
- Kieli
- Käännöksen osallistujat
- Osallistu käännökseen
- Auta meitä kääntämään %s kielellesi
- Luo pikakuvake, joka voi avata loishallintaohjelman.
- Pikakuvake kiinnitetty
- Nykyinen oletuskäynnistin ei tue pin-pikakuvakkeita.
- Tilailmoitus
- Näytä ilmoitus, joka voi avata loishallintaohjelman
- Päivitä kanava
- Vakaa
- Beeta
- Yöllinen rakentaminen
-
- Luennot
- Julkaisut
- Tiedot
- Kotisivu
- Lähdekoodi
- Yhteistyökumppanit
- Laitteet
- Avaa selaimessa
- Näytä vanhemmat versiot
- Ei enää versiota
- Ei voitu ladata moduulia repo: %s
- Päivitettävissä ensin
- Asennettu
-
- - %d lataa
- - %d lataukset
-
-
- Sakura
- Punainen
- Pinkki
- Violetti
- Syvä violetti
- Indigo
- Sininen
- Vaalea sininen
- Syaani
- Sinappi
- Vihreä
- Vaalea vihreä
- Limea
- Keltainen
- Meripihka
- Oranssi
- Syvä oranssi
- Ruskea
- Sininen harmaa
-
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
deleted file mode 100644
index da73d619e..000000000
--- a/app/src/main/res/values-fr/strings.xml
+++ /dev/null
@@ -1,244 +0,0 @@
-
-
-
-
- Aperçu
- Modules
-
- - %d module actif
- - %d modules actifs
-
- Journaux
- Réglages
- Réaction ou suggestion
- À propos
- Signaler un problème
- Dépôt
- Tous les modules sont à jour
- Publié le %s
- Mise à jour le %s
-
- - %d modules évolutifs
- - %d modules évolutifs
-
- Rejoindre notre canal %2$s]]>
- https://github.com/xerta555
-https://github.com/tclement0922
-JingMatrix
- Installer
- Appuyer pour installer LSPosed
- Non installé
- LSPosed n\'est pas installé
- Activé
- Partiellement activé
- SEPolicy n\’est pas chargé correctement
- Merci de ne pas remonter celà vers le développeur Magisk .]]>
- Échec de l\’injection du sous système
- Magisk ou certains modules Magisk de basse qualité. Essayez de désactiver les modules Magisk autres que Riru et LSPosed ou envoyez le journal complet aux développeurs.]]>
- Propriétés système incorrectes
- Des modules peuvent s\'invalider occasionnellement.]]>
- Mise à jour nécessaire
- Merci d\’installer la dernière version de LSPosed
- Conseils pour les développeurs de modules
- Veuillez désactiver les optimisations de déploiement sur Android Studio, ou utilisez la commande `gradlew installDebug` pour installer. Sinon, l\'APK du module ne sera pas mis à jour.
- Version de l\’API
- Version du framework
- Nom de paquet du gestionnaire
- Version du système
- Périphérique
- Architecture du système
- Enveloppeur Dex Optimizer
- Activé
- Non actif
- Supporté
- Non supporté
- Version d\'Android non satisfaisante
- Planté
- Échec du montage
- SELinux est permissif
- La politique SELinux est incorrecte
- Mettre à jour LSPosed
- Vous confirmez la mise à jour LSPosed ? Ce périphérique redémarrera après la mise à jour effectuée
- Copié dans le presse-papier
-
- Bienvenue dans LSPosed
- Vous utilisez le gestionnaire parasité, qui ne peut pas créer de raccourcis ou même être ouvert à partir d\'une notification.
- Vous utilisez le gestionnaire parasité, qui peut être ouvert depuis la notification.
- Créer le raccourci
- Ne jamais afficher
- Gestionnaire parasité recommandé
- LSPosed supporte maintenant la parasitage du système afin d\'éviter les détection, vous pouvez l\'ouvrir depuis la notification. Il est recommandé de désinstaller l\'application actuelle.
-
- Sauvegarder
- Journaux détaillés
- Journaux des modules
- Enregistrement du journal, veuillez patienter
- Journaux enregistrés
- Échec de la sauvegarde :\n%s
- Effacer le journal maintenant
- Journal effacé avec succès.
- Haut de page
- Chargement…
- Pied de page
- Recharger
- Échec de l\'effacement du journal
- Retour à la ligne
- Journaux détaillés activés
- Journaux détaillés désactivés
-
- (aucune description fournie)
- Ce module requière une nouvelle version d\'Xposed (%d) et n\'a donc pas pu être activé
- Ce module a été conçu pour une nouvelle version d\’Xposed (%d) et certaines fonctionnalités pourraient ne pas fonctionner
- Ce module ne spécifie pas la version d\'Xposed nécessaire.
- Ce module à été créé pour la version Xposed %1$d, mais due à des changements incompatibles dans la version %2$d, il à été désactivé
- Ce module ne peut pas être chargé car il est installé sur la carte SD, merci de le déplacer sur le stockage interne
- Désinstaller
- Réglages du module
- Afficher dans le dépôt
- Voulez-vous désinstaller ce module ?
- Désinstallation de %1$s
- Échec de la désinstallation
- Ajouter le module à l\’utilisateur
- %1$s ajouté à l’utilisateur %2$s
- Échec de l\’ajout du module
- Installer dans l\'utilisateur %s
- Vous voulez installer %1$s dans l\'utilisateur %2$s ? Il est recommandé de l\'installer manuellement, forcer l\'installation via LSPosed pourrait causer des problèmes.
- développer
- réduire
-
- Ré-optimiser
- Optimisation…
- Optimisation terminée
- Démarrer
- Échec de l\’optimisation : la valeur renvoyée est vide
- Échec de l\’optimisation :
- Trier par nom d\’application
- Trier par nom de paquet
- Trier par date d\’installation
- Trier par heure de mise à jour
- Inversé
- Applications système
- Trier
- Activer le module
- Vous n\'avez sélectionné aucune application. Continuer ?
- Jeux
- Modules
- Échec de l\'enregistrement de la liste des périmètres d\'applications
- Version : %1$s
- Choisir
- Recommandé
- Vous n\'avez sélectionné aucune application. Sélectionner les applications recommandées ?
- Sélectionner les applications recommandées ?
- Toutes
- Aucune
- Inclus auto
- Le module Xposed n\’est pas encore activé
- Recommandé
- Mise à jour disponible : %1$s
- Le module %s a été désactivé étant donné qu\’aucune application n\’ai été sélectionné.
- Cadre du sous-système
- Sauvegarde
- Sauvegarder
- Restaurer
- Forcer l\’arrêt
- Forcer l\'arrêt ?
- Si vous forcez l\'arrêt d\'une application, celle-ci pourrait mal fonctionner.
- Un redémarrage est requis pour appliquer les changements
- Redémarrer
- Masquage
-
- Afficher dans une autre application
- Informations d\’application
- ¯\\\\_(ツ)_\/¯\nIl n\’y a rien ici
-
- Sous-système
- Désactiver les journaux détaillés
- Les journaux détaillés sont requis pour signaler des problèmes
- Thème noir et sombre
- Utiliser le thème noir pur si le thème noir est activé
- Thème
- Sauvegarder et restaurer
- Sauvegarder la liste des modules ainsi que leurs champs d\'applications.
- Restaurer la liste des modules ainsi que leurs champs d\'applications.
- Sauvegarder
- Échec de la sauvegarde :\n%s
- Merci d\'activer le gestionnaire de fichiers
- Restaurer
- Échec de la restauration :\n%s
- Réseau
- DNS sur HTTPS
- Contourner la censure DNS dans certains pays
- Couleur du thème
- Couleur d\'accentuation du système
- Forcer les applications à afficher leurs icônes dans le lanceur
- Après Android 10, les applications ne sont pas autorisées à masquer leurs icônes dans le lanceur. Désactiver ce commutateur pour désactiver cette fonctionnalité du système.
- Système
- Langage
- Contributeurs de traduction
- Participer à la traduction
- Aidez-nous à traduire %s dans votre langue
- Créer un raccourci qui peut ouvrir le gestionnaire de parasites
- Raccourci épinglé
- Le lanceur par défaut actuel ne supporte pas les raccourcis épinglés
- Notification d\'état
- Afficher une notification qui peut ouvrir le gestionnaire de parasites
- Canal de mise à jour
- Stable
- Bêta
- Alpha
-
- Lisez-moi
- Versions
- Infos
- Page d\’accueil
- Code source
- Collaborateurs
- Actifs
- Ouvrir dans le navigateur
- Afficher les anciennes versions
- Pas d\’autres versions
- Échec de chargement du dépôt des modules : %s
- Évolutifs en premier
- installée
-
- - %d téléchargé
- - %d téléchargés
-
-
- Sakura
- Rouge
- Rose
- Violet
- Violet foncé
- Indigo
- Bleu
- Bleu clair
- Cyan
- Turquoise
- Vert
- Vert clair
- Vert citron
- Jaune
- Ambre
- Orange
- Orange foncé
- Marron
- Bleu grisâtre
-
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
deleted file mode 100644
index a8c7c5b28..000000000
--- a/app/src/main/res/values-hi/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- ओवरव्यू
- मॉड्यूल्स
-
- - %d मॉड्यूल एनेबल किए गए
- - %d मॉड्यूल्स एनेबल किए गए
-
- लॉग्स
- सेटिंग्स
- फीडबैक या सजेशन
- इसके बारे में
- इशू को रिपोर्ट करें
- रिपोज़िटरी
- सभी मॉड्यूल अप टू डेट
- %s. पर प्रकाशित
- %s. पर अपडेट किया गया
-
- - %d मॉड्यूल अपग्रेड करने योग्य
- - %d मॉड्यूल अपग्रेड करने योग्य
-
- पर सोर्स कोड देखें हमारे %2$s चैनल से जुड़ें]]>
- Ahmad Shaikh
- स्थापित करना
- LSPosed स्थापित करने के लिए टैप करें
- स्थापित नहीं हे
- LSPosed स्थापित नहीं है
- सक्रिय
- आंशिक रूप से सक्रिय
- SEPolicy ठीक से लोड नहीं है
- कृपया इसकी सूचना मैजिक डेवलपर को दें।]]>
- सिस्टम फ्रेमवर्क इंजेक्शन विफल
- Magisk या कुछ निम्न-गुणवत्ता वाले Magisk मॉड्यूल के कारण हो सकता है। कृपया Riru और LSPosed के अलावा अन्य Magisk मॉड्यूल को अक्षम करने का प्रयास करें या डेवलपर्स को पूर्ण लॉग सबमिट करें।]]>
- सिस्टम प्रोप गलत
- मॉड्यूल कभी-कभी अमान्य हो सकते हैं।]]>
- अद्यतन करने की आवश्यकता है
- कृपया LSPosed का नवीनतम संस्करण स्थापित करें
- एपीआई संस्करण
- फ्रेमवर्क संस्करण
- प्रबंधक पैकेज का नाम
- सिस्टम संस्करण
- उपकरण
- सिस्टम एबीआई
- डेक्स ऑप्टिमाइज़र रैपर
- सक्रिय
- निष्क्रिय
- समर्थित
- असमर्थित
- Android संस्करण असंतुष्ट
- दुर्घटनाग्रस्त
- माउंट विफल
- SELinux अनुमेय है
- SELinux नीति गलत है
- अद्यतन LSPosed
- LSPosed को अपडेट करने की पुष्टि करें? अपडेट पूरा होने के बाद यह डिवाइस रीबूट हो जाएगा
- क्लिपबोर्ड पर नकल
-
- LSPosed में आपका स्वागत है
- आप परजीवी प्रबंधक का उपयोग कर रहे हैं, जो शॉर्टकट बना सकता है या सूचना से अभी भी खुला हो सकता है।
- आप परजीवी प्रबंधक का उपयोग कर रहे हैं, जो सूचना से खुल सकता है।
- शॉर्टकट बनाएं
- कभी भी न दिखाओ
- परजीवी प्रबंधक की सिफारिश की
- LSPosed अब पता लगाने से बचने के लिए सिस्टम परजीवीकरण का समर्थन करता है, आप अधिसूचना से परजीवी प्रबंधक खोल सकते हैं। वर्तमान एप्लिकेशन को अनइंस्टॉल करने की अनुशंसा की जाती है।
-
- बचाना
- वर्बोज़ लॉग्स
- मॉड्यूल लॉग
- लॉग सहेजा जा रहा है, कृपया प्रतीक्षा करें
- लॉग सेव हो गए
- सहेजने में विफल:\n%s
- अभी लॉग साफ़ करें
- लॉग सफलतापूर्वक साफ़ किया गया।
- शीर्ष तक स्क्रॉल करें
- लोड हो रहा है…
- नीचे स्क्रॉल करें
- पुनः लोड करें
- लॉग साफ़ करने में विफल
- वर्ड रैप
- वर्बोज़ लॉग सक्षम
- वर्बोज़ लॉग अक्षम
-
- (कोई विवरण नहीं दिया गया)
- इस मॉड्यूल को एक नए Xposed संस्करण (%d) की आवश्यकता है और इस प्रकार इसे सक्रिय नहीं किया जा सकता है
- यह मॉड्यूल एक नए Xposed संस्करण (%d) के लिए डिज़ाइन किया गया है और इस प्रकार कुछ कार्यात्मकताएँ काम नहीं कर सकती हैं
- यह मॉड्यूल Xposed संस्करण को निर्दिष्ट नहीं करता है जिसकी उसे आवश्यकता है।
- यह मॉड्यूल Xposed संस्करण %1$dके लिए बनाया गया था, लेकिन संस्करण %2$dमें असंगत परिवर्तनों के कारण, इसे अक्षम कर दिया गया है
- यह मॉड्यूल लोड नहीं किया जा सकता क्योंकि यह एसडी कार्ड पर स्थापित है, कृपया इसे आंतरिक भंडारण में ले जाएं
- स्थापना रद्द करें
- मॉड्यूल सेटिंग्स
- रेपो में देखें
- क्या आप इस मॉड्यूल को अनइंस्टॉल करना चाहते हैं?
- अनइंस्टॉल किया गया %1$s
- अनइंस्टॉल असफल
- उपयोगकर्ता में मॉड्यूल जोड़ें
- उपयोगकर्ता %2$sमें %1$s जोड़ा गया
- मॉड्यूल जोड़ना विफल
- उपयोगकर्ता को स्थापित करें %s
- उपयोगकर्ता %2$sपर %1$s स्थापित करना चाहते हैं? मैन्युअल रूप से स्थापित करने की अनुशंसा की जाती है, LSPosed के माध्यम से स्थापना को मजबूर करने से समस्या हो सकती है।
- विस्तार
- ढहना
-
- पुन: अनुकूलित
- अनुकूलन…
- अनुकूलन पूर्ण
- इसे लॉन्च करें
- अनुकूलन विफल: वापसी मूल्य खाली है
- अनुकूलन विफल:
- आवेदन का नाम
- पैकेज का नाम
- समय स्थापित करें
- समय सुधारें
- उल्टा
- सिस्टम ऐप्स
- छंटाई
- मॉड्यूल सक्षम करें
- आपने कोई ऐप नहीं चुना है। जारी रखें?
- खेल
- मॉड्यूल
- कार्यक्षेत्र सूची सहेजने में विफल
- संस्करण: %1$s
- अनुशंसित
- आपने कोई ऐप नहीं चुना है। अनुशंसित ऐप्स चुनें?
- अनुशंसित ऐप्स चुनें?
- एक्सपोज़ड मॉड्यूल अभी तक सक्रिय नहीं है
- अनुशंसित
- अपडेट उपलब्ध: %1$s
- मॉड्यूल %s को अक्षम कर दिया गया है क्योंकि कोई ऐप नहीं चुना गया है।
- सिस्टम फ्रेमवर्क
- बैकअप
- बैकअप
- पुनर्स्थापित करना
- जबर्दस्ती बंद करें
- जबर्दस्ती बंद करें?
- यदि आप किसी ऐप को जबरदस्ती बंद करते हैं, तो वह गलत व्यवहार कर सकता है।
- इस परिवर्तन को लागू करने के लिए रीबूट की आवश्यकता है
- रीबूट
- छिपाना
-
- अन्य ऐप में देखें
- अनुप्रयोग की जानकारी
- ¯\\\\_(ツ)_\/¯\nयहाँ कुछ भी नहीं
-
- रूपरेखा
- वर्बोज़ लॉग अक्षम करें
- रिपोर्ट वर्बोज़ लॉग शामिल करने का अनुरोध जारी करती है
- ब्लैक डार्क थीम
- यदि डार्क थीम सक्षम है तो शुद्ध काली थीम का उपयोग करें
- थीम
- बैकअप और पुनर्स्थापना
- बैकअप मॉड्यूल सूची और कार्यक्षेत्र सूचियाँ।
- मॉड्यूल सूची और कार्यक्षेत्र सूचियों को पुनर्स्थापित करें।
- बैकअप
- बैकअप में विफल:\n%s
- कृपया DocumentUI सक्षम करें
- पुनर्स्थापित करना
- पुनर्स्थापित करने में विफल:\n%s
- नेटवर्क
- एचटीटीपीएस पर डीएनएस
- कुछ देशों में DNS विषाक्तता का समाधान
- थीम रंग
- सिस्टम थीम रंग
- लॉन्चर आइकन दिखाने के लिए ऐप्स को बाध्य करें
- Android 10 के बाद, ऐप्स को अपने लॉन्चर आइकन छिपाने की अनुमति नहीं है। इस सिस्टम सुविधा को अक्षम करने के लिए टॉगल बंद करें।
- प्रणाली
- भाषा
- अनुवाद योगदानकर्ता
- अनुवाद में भाग लें
- %s को अपनी भाषा में अनुवाद करने में हमारी सहायता करें
- एक शॉर्टकट बनाएं जो परजीवी प्रबंधक खोल सके
- शॉर्टकट पिन किया गया
- वर्तमान डिफ़ॉल्ट लांचर पिन शॉर्टकट का समर्थन नहीं करता
- स्थिति अधिसूचना
- एक अधिसूचना दिखाएं जो परजीवी प्रबंधक खोल सकती है
- चैनल अपडेट करें
- स्थिर
- बीटा
- सॉफ़्टवेयर की स्थिरता
-
- रीडमी
- विज्ञप्ति
- जानकारी
- होमपेज
- सोर्स कोड
- सहयोगियों
- संपत्तियां
- ब्राउज़र में खोलें
- पुराने संस्करण दिखाएं
- कोई और रिलीज नहीं
- मॉड्यूल रेपो लोड करने में विफल: %s
- पहले अपग्रेड करने योग्य
- स्थापित
-
- - %d डाउनलोड
- - %d डाउनलोड
-
-
- सकुरा
- लाल
- गुलाबी
- बैंगनी
- गहरा बैंगनी
- नील
- नीला
- हल्का नीला रंग
- सियान
- टील
- हरा
- हल्का हरा
- नींबू
- पीला
- अंबर
- संतरा
- गहरा नारंगी
- भूरा
- नीला ग्रे
-
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
deleted file mode 100644
index b22c54967..000000000
--- a/app/src/main/res/values-hr/strings.xml
+++ /dev/null
@@ -1,239 +0,0 @@
-
-
-
-
- Pregled
- Moduli
-
- - %d modul omogućen
- - %d modula omogućeno
- - %d modula omogućeno
-
- Zapisi
- Postavke
- Povratna informacija ili prijedlog
- Informacije
- Prijavi problem
- Spremište modula
- Svi moduli ažurirani
- Objavljeno u %s
- Ažurirano u %s
-
- - %d modul moguće nadograditi
- - %d modula moguće nadograditi
- - %d modula moguće nadograditi
-
- Pridružite se našem %2$s kanalu]]>
- https://github.com/cube2412
- Instaliraj
- Dodirnite za instaliranje LSPosed
- Nije instalirano
- LSPosed nije instaliran
- Aktiviran
- Djelomično aktiviran
- SEPolicy nije pravilno učitan
- Prijavite ovo Magisk programeru.]]>
- Injektiranje u System Framework nije uspjelo
- Magisk ili nekim Magisk modulima niske kvalitete. Pokušajte onemogućiti Magisk module koji nisu Riru i LSPosed ili pošaljite cijeli zapis programerima.]]>
- Svojstva sustava nisu ispravna
- Moduli povremeno mogu biti nedostupni.]]>
- Treba ažurirati
- Molimo instalirajte najnoviju verziju LSPosed
- API verzija
- Framework verzija
- Naziv paketa upravitelja
- System verzija
- Uređaj
- System ABI
- Dex Optimizer Wrapper
- Omogućeno
- Nije omogućeno
- Podržano
- Nepodržano
- Verzija Androida nije zadovoljavajuća
- Srušio se
- Postavljanje nije uspjelo
- SELinux je permisivan
- Pravila SELinuxa je netočna
- Ažurirajte LSPosed
- Potvrditi ažuriranje LSPosed? Ovaj će se uređaj ponovno pokrenuti nakon završetka ažuriranja
- Kopirano u međuspremnik
-
- Dobrodošli u LSPosed
- Koristite parazitski upravitelj, koji može stvoriti prečac ili još uvijek otvoriti iz obavijesti.
- Koristite parazitski upravitelj koji se može otvoriti iz obavijesti.
- Napravi prečac
- Nikad ne pokazuj
- Parazitski Manager Preporučen
- LSPosed sada podržava parazitizaciju sustava kako bi se izbjeglo otkrivanje, možete otvoriti parazitski upravitelj iz obavijesti. Preporuča se deinstalirati trenutnu aplikaciju.
-
- Sačuvaj
- Opširni zapisi
- Zapisi Modula
- Spremanje dnevnika, pričekajte
- Zapisi spremljeni
- Neuspješno spremanje:\n%s
- Očisti zapis sada
- Zapis je uspješno izbrisan.
- Pomaknite se na vrh
- Učitavanje…
- Pomaknite se do dna
- Ponovno učitaj
- Brisanje zapisa nije uspjelo
- Prijelom riječi
- Opširni zapis omogućen
- Opširni zapis onemogućen
-
- (nema opisa)
- Ovaj modul zahtijeva noviju verziju Xposed (%d) i stoga se ne može aktivirati
- Ovaj modul je dizajniran za noviju verziju Xposed (%d) i stoga neke funkcije možda neće raditi
- Ovaj modul ne navodi verziju Xposed koja mu je potrebna.
- Ovaj modul je stvoren za Xposed verziju %1$d, zbog nekompatibilnih promjena u verziji %2$d, modul je onemogućen
- Ovaj modul nije moguće učitati jer je instaliran na SD kartici, molimo premjestite ga u internu memoriju
- Deinstaliraj
- Postavke modula
- Pogledaj u Repou
- Želite li deinstalirati ovaj modul?
- Deinstalirano %1$s
- Deinstalacija nije uspjela
- Dodavanje modula korisniku
- Dodano %1$s korisniku %2$s
- Dodavanje modula nije uspjelo
- Instaliraj na korisnika %s
- Želite li instalirati %1$s korisniku %2$s? Preporuča se ručna instalacija, prisilna instalacija putem LSPoseda može uzrokovati probleme.
- proširi
- sklopi
-
- Ponovno optimiziraj
- Optimizacija…
- Optimizacija dovršena
- Pokreni ga
- Optimizacija nije uspjela: povratna vrijednost je prazna
- Optimizacija nije uspjela:
- Naziv aplikacije
- Naziv paketa
- Vrijeme instalacije
- Vrijeme ažuriranja
- Obrnuto
- Aplikacije sustava
- Sortiranje
- Omogući modul
- Niste odabrali nijednu aplikaciju. Nastaviti?
- Igre
- Moduli
- Spremanje popisa opsega primjene nije uspjelo
- Verzija: %1$s
- Preporučeno
- Niste odabrali nijednu aplikaciju. Odaberi preporučene aplikacije?
- Odaberi preporučene aplikacije?
- Xposed modul još nije aktiviran
- Preporučeno
- Dostupno ažuriranje: %1$s
- Modul %s je onemogućen jer nije odabrana nijedna aplikacija.
- System Framework
- Sigurnosna kopija
- Sigurnosna kopija
- Vrati sigurnosnu kopiju
- Prisilno zaustavi
- Prisilno zaustaviti?
- Ako prisilno zaustavite aplikaciju, može doći do nepredvidivog ponašanja.
- Za primjenu ove promjene potrebno je ponovno pokretanje
- Ponovno podizanje sustava
- Sakrij
-
- Pogledaj u drugoj aplikaciji
- Informacije o aplikaciji
- ¯\\\\_(ツ)_\/¯\nOvdje nema ničega
-
- Framework
- Onemogući opširne zapise
- Izvješće o problemima zahtijeva uključivanje opširnih zapisa
- Crna tamna tema
- Koristi čistu crnu temu ako je tamna tema omogućena
- Tema
- Sigurnosno kopiranje i vraćanje
- Lista sigurnosnih kopija modula i opširne liste.
- Lista modula vraćenih iz sigurnosne kopije i opsežne liste.
- Sigurnosna kopija
- Sigurnosno kopiranje nije uspjelo:\n%s
- Molimo omogućite DocumentUI
- Vrati
- Nije uspjelo vraćanje:\n%s
- Mreža
- DNS preko HTTPS-a
- Zaobilazno rješenje DNS trovanja u nekim zemljama
- Boja teme
- Boja teme sustava
- Prisilite aplikacije da prikazuju ikone pokretača
- Nakon Androida 10 aplikacijama nije dopušteno skrivanje ikona pokretača. Isključite prekidač da biste onemogućili ovu značajku sustava.
- Sustav
- Jezik
- Suradnici prijevoda
- Sudjelujte u prevođenju
- Pomozite nam prevesti %s na vaš jezik
- Napravite prečac koji može otvoriti parazitski upravitelj
- Prečac prikvačen
- Trenutačni zadani pokretač ne podržava prečace pribadače
- Obavijest o statusu
- Prikaži obavijest koja može otvoriti parazitski upravitelj
- Ažurirajte kanal
- Stabilan
- Beta
- Noćna izgradnja
-
- Pročitaj me
- Izdanja
- Info
- Početna stranica
- Izvorni kod
- Suradnici
- Imovina
- Otvori u pretraživaču
- Prikaži starije verzije
- Nema više puštanja
- Neuspješno učitavanje spremišta modula: %s
- Prvo nadogradivo
- instalirano
-
- - %d preuzimanje
- - %d preuzimanja
- - %d preuzimanja
-
-
- Sakura
- Crvena
- Ružičasta
- Ljubičasta
- Tamno ljubičasta
- Indigo
- Plava
- Svijetlo plava
- cijan
- Teal
- zelena
- Svijetlo zelena
- Vapno
- Žuta boja
- jantar
- naranča
- Tamno narančasta
- Smeđa
- Plavo siva
-
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
deleted file mode 100644
index c70ee1d2a..000000000
--- a/app/src/main/res/values-hu/strings.xml
+++ /dev/null
@@ -1,237 +0,0 @@
-
-
-
-
- Áttekintés
- Modulok
-
- - %d modul aktiválva
- - %d modulok aktiválva
-
- Napló fájlok
- Beállítások
- Visszajelzés vagy javaslat
- Névjegy
- Hibabejelentés
- Modulok
- Minden modul naprakész
- Közzétéve: %s
- Frissítve: %s
-
- - %d modul frissíthető
- - %d modul frissíthető
-
- Iratkozz fel a %2$s csatornánkra]]>
- عبدو المكحل, Balázs Juhász, Krisztián Molnár
- Telepítés
- Koppints az LSPosed telepítéséhez
- Nincs telepítve
- Az LSPosed nincs telepítve
- Aktiválva
- Részben aktiválva
- Az SEPolicy nem töltött be megfelelően
- Kérjük, jelezze ezt a Magisk fejlesztőnek.]]>
- A System Framework injektálása sikertelen
- Magisk vagy néhány Magisk modul okozott. Kérlek próbálj meg deaktiválni néhány Magisk modult a Riru és LSPosed modulokon kívül vagy küldj el egy teljes napló fájlt a fejlesztőknek.]]>
- Helytelen rendszertulajdonságok
- A modulok időnként érvénytelenné válhatnak.]]>
- Frissítésre van szükség
- Kérjük, telepítse az LSPosed legújabb verzióját
- API verzió
- Keretrendszer verzió
- Menedzser csomag neve
- Rendszer verzió
- Eszköz
- Rendszer ABI
- Dex Optimizer Wrapper
- Engedélyezve
- Nincs engedélyezve
- Támogatott
- Nem támogatott
- Az Android verzió nem megfelelő
- Összeomlott
- A csatolás sikertelen
- Az SELinux engedélyezett
- Az SELinux házirend helytelen
- Az LSPosed frissítése
- Jóváhagyja az LSPosed frissítését? A készülék a frissítés befejezése után újra fog indulni
- A vágólapra másolva
-
- Üdvözöljük az LSPosed
- Ön használja a parazita menedzser, amely képes létrehozni parancsikont vagy még mindig nyitva értesítésből.
- Ön a parazita-kezelőt használja, amely az értesítésből megnyitható.
- Parancsikon létrehozása
- Soha ne mutassa
- Parazita menedzser Ajánlott
- Az LSPosed mostantól támogatja a rendszerparazitizációt a felismerés elkerülése érdekében, a parazita-kezelőt az értesítésből nyithatja meg. Javasoljuk, hogy távolítsa el az aktuális alkalmazást.
-
- Mentés
- Szöveges naplók
- Modulok Naplói
- Napló mentése, kérem várjon
- Mentett naplók
- Nem sikerült menteni:\n%s
- Törölje a naplót most
- A napló sikeresen törlődött.
- Görgessen a tetejére
- Betöltés…
- Görgessen az aljára
- Újratöltés
- Nem sikerült törölni a naplót
- Word Wrap
- Szöveges napló engedélyezve
- Szöveges napló letiltva
-
- (nincs leírás megadva)
- Ez a modul egy újabb Xposed verziót igényel (%d), ezért nem aktiválható
- Ez a modul egy újabb Xposed verzióhoz készült (%d), ezért előfordulhat, hogy egyes funkciók nem működnek
- Ez a modul nem határoz meg szükséges Xposed verziót.
- Ez a modul az Xposed %1$dverziójához készült, de a %2$dverzióban bekövetkezett inkompatibilis változások miatt letiltásra került.
- Ez a modul nem tölthető be, mert az SD-kártyára van telepítve, kérjük, helyezze át a belső tárhelyre.
- Eltávolítás
- Modul beállítások
- Megtekintés a Repóban
- Szeretné eltávolítani ezt a modult?
- %1$s eltávolítva
- Az eltávolítás sikertelen
- Modul hozzáadása a felhasználóhoz
- %1$s hozzáadva a(z) %2$s felhasználóhoz
- A modul hozzáadása sikertelen
- Telepítés a felhasználóhoz %s
- Szeretné telepíteni a %1$s -t a %2$sfelhasználóhoz ? Javasoljuk a manuális telepítést, az LSPosed-en keresztül történő kényszerített telepítés problémákat okozhat.
- kiterjesztés
- összecsukás
-
- Újraoptimalizálás
- Optimalizálás…
- Az optimalizálás befejezve
- Indítsd el
- Optimalizálás sikertelen: a visszatérési érték üres
- Az optimalizálás nem sikerült:
- Alkalmazás neve
- Csomag neve
- Telepítés ideje
- Frissítés ideje
- Fordított
- Rendszeralkalmazások
- Rendezés
- Modul engedélyezése
- Nem választott ki egyetlen alkalmazást sem. Folytatja?
- Játékok
- Modulok
- Nem sikerült elmenteni a hatókör listát
- Verzió: %1$s
- Ajánlott
- Nem választott ki egyetlen alkalmazást sem. Kiválasztja az ajánlott alkalmazásokat?
- Kiválasztja az ajánlott alkalmazásokat?
- Az Xposed modul még nincs aktiválva
- Ajánlott
- Frissítés elérhető: %1$s
- A %s modul le lett tiltva, mivel nincs kiválasztott alkalmazás.
- Rendszer keretrendszer
- Biztonsági mentés
- Biztonsági mentés
- Visszaállítás
- Erőszakos megállás
- Erőszakos megállás?
- Ha egy alkalmazást erőltetett leállítással állít le, az rosszul viselkedhet.
- A módosítás érvényesítéséhez újraindítás szükséges
- Újraindítás
- Rejtsd el
-
- Megtekintés más alkalmazásban
- Alkalmazás információ
- ¯\\\\_(ツ)_\/¯\nItt nincs semmi
-
- Keretrendszer
- A szöveges naplózás kikapcsolása
- Jelentési kérdések kérése a verbózus naplók felvételére
- Fekete sötét téma
- Teljesen fekete téma használata, ha a sötét téma engedélyezve van
- Téma
- Biztonsági mentés és visszaállítás
- A modul lista és hatókör listák biztonsági mentése.
- A modullista és a hatókörlisták visszaállítása.
- Biztonsági mentés
- A biztonsági mentés sikertelen:\n%s
- Kérjük, engedélyezze a DocumentUI-t
- Visszaállítás
- Nem sikerült visszaállítani:\n%s
- Hálózat
- DNS HTTPS-en keresztül
- Megoldás DNS-mérgezés esetén egyes országokban
- Téma színe
- Rendszertéma színe
- Az alkalmazások kényszerítése az indító ikonok megjelenítésére
- Az Android 10 után az alkalmazások nem rejthetik el az indítóikonjaikat. Kapcsolja ki a kapcsolót ennek a rendszerfunkciónak a letiltásához.
- Rendszer
- Nyelv
- A fordításban közreműködők
- Részvétel a fordításban
- Segítsen nekünk lefordítani az %s -t az Ön nyelvére
- Hozzon létre egy parancsikont, amely képes megnyitni a parazita menedzser
- Parancsikon kitüzve
- A jelenlegi alapértelmezett indítóprogram nem támogatja a pin parancsikonokat
- Állapot értesítés
- Értesítés megjelenítése, amely megnyithatja a parazita-kezelőt
- Frissítési csatorna
- Stabil
- Béta
- Nightly
-
- Olvass el
- Kiadások
- Információ
- Honlap
- Forráskód
- Együttműködők
- Eszközök
- Megnyitás a böngészőben
- Régebbi verziók megjelenítése
- Nincs több kiadás
- Nem sikerült betölteni a modul repo-t: %s
- Frissíthetőek először
- Telepítve
-
- - %d letöltés
- - %d letöltések
-
-
- Sakura
- Piros
- Rózsaszín
- Lila
- Mély lila
- Indigo
- Kék
- Világoskék
- Cián
- Zöldeskék
- Zöld
- Világoszöld
- Lime
- Sárga
- Borostyán
- Narancs
- Mély narancssárga
- Barna
- Kékes szürke
-
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
deleted file mode 100644
index 204ce2c59..000000000
--- a/app/src/main/res/values-in/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Ringkasan
- Modul
-
- - %d modul diaktifkan
-
- Log
- Pengaturan
- Umpan balik atau saran
- Tentang
- Laporkan masalah
- Gudang
- Semua modul sudah terbaru
- Diterbitkan pada %s
- Diperbarui pada %s
-
- - %d modul dapat ditingkatkan
-
- Gabung dengan kami di saluran %2$s]]>
- pɹɐɥllıʇS
- Pasang
- Ketuk untuk memasang LSPosed
- Tidak terpasang
- LSPosed tidak terpasang
- Diaktifkan
- Diaktifkan sebagian
- SEPolicy tidak dimuat dengan benar
- Harap laporkan ini ke pengembang Magisk .]]>
- Injeksi Kerangka Sistem gagal
- Magisk atau beberapa modul Magisk berkualitas rendah. Coba nonaktifkan modul Magisk selain Riru dan LSPosed atau kirimkan log lengkap ke pengembang.]]>
- Prop sistem salah
- Modul terkadang tidak valid.]]>
- Butuh pembaruan
- Silakan pasang LSPosed versi terbaru
- Tips untuk pengembang modul
- Harap nonaktifkan pengoptimalan penerapan di Android Studio, atau gunakan perintah `gradlew installDebug` untuk menginstal. Jika tidak, apk modul tidak akan diperbarui.
- Versi API
- Versi framework
- Nama paket manajer
- Versi sistem
- Perangkat
- Sistem ABI
- Pengoptimal Pengemas Dex
- Diaktifkan
- Tidak diaktifkan
- Didukung
- Tidak didukung
- Versi Android tidak tersedia
- Rusak
- Mount gagal
- SELinux permisif
- SELinux policy salah
- Perbarui LSPosed
- Konfirmasi untuk pembaruan LSPosed? Perangkat ini akan mulai ulang setelah pembaruan selesai
- Disalin ke papan klip
-
- Selamat datang di LSPosed
- Anda menggunakan manajer parasit, yang dapat membuat pintasan atau dapat terbuka dari notifikasi.
- Anda menggunakan manajer parasit, yang dapat dibuka dari notifikasi.
- Buat pintasan
- Jangan pernah tampilkan
- Manajer Parasit Direkomendasikan
- LSPosed sekarang mendukung parasitisasi sistem untuk menghindari deteksi, Anda dapat membuka manajer parasit dari pemberitahuan. Disarankan untuk menghapus aplikasi saat ini.
-
- Simpan
- Log Verbose
- Log Modul
- Menyimpan log, harap tunggu
- Log disimpan
- Gagal menyimpan:\n%s
- Hapus log sekarang
- Log berhasil dihapus.
- Gulir ke atas
- Memuat…
- Gulir ke bawah
- Muat ulang
- Gagal menghapus log
- Bungkus Kata
- Log verbose diaktifkan
- Log verbose dinonaktifkan
-
- (tidak ada deskripsi yang diberikan)
- Modul ini memerlukan versi Xposed yang lebih baru (%d) sehingga tidak dapat diaktifkan
- Modul ini dirancang untuk versi Xposed yang lebih baru (%d) sehingga beberapa fungsi mungkin tidak berfungsi
- Modul ini tidak menentukan versi Xposed yang diperlukan.
- Modul ini dibuat untuk Xposed versi %1$d, tetapi karena perubahan yang tidak kompatibel di versi %2$d, modul ini telah dinonaktifkanModul ini dibuat untuk Xposed versi %1$d, tetapi karena perubahan yang tidak kompatibel di versi %2$d, modul ini telah dinonaktifkan
- Modul ini tidak dapat dimuat karena terpasang di kartu SD, harap pindahkan ke penyimpanan internal
- Copot
- Pengaturan modul
- Lihat di Repo
- Apakah Anda ingin mencopot modul ini?
- Tercopot %1$s
- Copot pemasangan tidak berhasil
- Tambahkan modul ke pengguna
- Ditambahkan %1$s ke pengguna %2$s
- Gagal menambahkan modul
- Pasang ke pengguna %s
- Ingin memasang %1$s ke pengguna %2$s? Disarankan untuk memasang secara manual, memaksa pemasangan melalui LSPosed dapat menyebabkan masalah.
- perluas
- ciutkan
-
- Mengoptimalkan ulang
- Mengoptimalkan…
- Optimalisasi selesai
- Luncurkan
- Optimalisasi gagal: nilai yang dihasilkan kosong
- Optimalisasi gagal:
- Nama aplikasi
- Nama paket
- Waktu pemasangan
- Waktu pembaruan
- Terbalik
- Aplikasi sistem
- Penyortiran
- Aktifkan modul
- Anda tidak memilih aplikasi apa pun. Lanjutkan?
- Permainan
- Modul
- Gagal menyimpan ke daftar cakupan
- Versi: %1$s
- Direkomendasikan
- Anda tidak memilih aplikasi apapun. Pilih aplikasi yang disarankan?
- Pilih aplikasi yang disarankan?
- Modul Xposed belum diaktifkan
- Direkomendasikan
- Pembaruan tersedia: %1$s
- Modul %s telah dinonaktifkan karena tidak ada aplikasi yang dipilih.
- Kerangka kerja sistem
- Cadangan
- Cadangkan
- Pulihkan
- Paksa berhenti
- Paksa berhenti?
- Jika Anda menghentikan paksa aplikasi, mungkin dapat bekerja tidak semestinya.
- Mulai ulang diperlukan agar perubahan ini dapat diterapkan
- Mulai ulang
- Sembunyikan
-
- Lihat di aplikasi lain
- Informasi aplikasi
- ¯\\\\_(ツ)_\/¯\nTidak ada apa-apa di sini
-
- Kerangka kerja
- Nonaktifkan log verbose
- Permintaan laporan masalah dengan menyertakan log-log verbose
- Tema hitam gelap
- Gunakan tema hitam murni jika tema gelap diaktifkan
- Tema
- Cadangkan dan pulihkan
- Cadangkan daftar modul dan daftar cakupan.
- Pulihkan daftar modul dan daftar cakupan.
- Cadangkan
- Gagal mencadangkan:\n%s
- Harap aktifkan DocumentUI
- Pulihkan
- Gagal memulihkan:\n%s
- Jaringan
- DNS melalui HTTPS
- Solusi mengatasi masalah DNS di beberapa negara
- Warna tema
- Warna tema sistem
- Paksa aplikasi untuk menampilkan ikon peluncur
- Setelah Android 10, aplikasi tidak diizinkan menyembunyikan ikon peluncurnya. Matikan untuk menonaktifkan fitur sistem ini.
- Sistem
- Bahasa
- Kontributor terjemahan
- Berpartisipasi dalam terjemahan
- Bantu kami menerjemahkan %s ke dalam bahasamu
- Buat pintasan yang dapat membuka manajer parasit
- Pintasan disematkan
- Peluncur default saat ini tidak mendukung pintasan pin
- Notifikasi Status
- Tampilkan notifikasi yang dapat membuka manajer parasit
- Perbarui saluran
- Stabil
- Beta
- Rilis harian
-
- Baca aku
- Rilis
- Informasi
- Beranda
- Kode sumber
- Kolaborator
- Aset
- Buka di browser
- Tampilkan versi lama
- Tidak ada rilis
- Gagal memuat repo modul: %s
- Dapat diupgrade terlebih dahulu
- Terpasang
-
- - %d unduh
-%d unduhan
-
-
- Sakura
- Merah
- Merah muda
- Ungu
- Ungu gelap
- Biru gelap
- Biru
- Biru muda
- Cyan
- Hijau toska
- Hijau
- Hijau muda
- Hijau limau
- Kuning
- Kuning madu
- Jingga
- Jingga gelap
- Coklat
- Abu-abu kebiruan
-
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
deleted file mode 100644
index b1032c154..000000000
--- a/app/src/main/res/values-it/strings.xml
+++ /dev/null
@@ -1,238 +0,0 @@
-
-
-
-
- Panoramica
- Moduli
-
- - %d modulo abilitato
- - %d moduli abilitati
-
- Log
- Impostazioni
- Feedback o suggerimenti
- Informazioni
- Segnala il problema
- Repository
- Tutti i moduli sono aggiornati
- Pubblicato alle %s
- Aggiornato alle %s
-
- - %d modulo aggiornabile
- - %d moduli aggiornabili
-
- Unisciti al nostro canale %2$s]]>
- alex193a, Fs00, xDonatello
- Installa
- Tocca per installare LSPosed
- Non installato
- LSPosed non è installato
- Attivo
- Parzialmente attivo
- SEPolicy non è caricato correttamente
- Segnalalo allo sviluppatore di Magisk .]]>
- Injection del framework di sistema fallita
- Questo problema si verifica raramente e può essere causato da Magisk o da alcuni moduli Magisk di scarsa qualità. Prova a disabilitare i moduli Magisk tranne Riru e LSPosed o invia il log completo agli sviluppatori.]]>
- Proprietà di sistema errate
- In alcuni casi i moduli potrebbero non funzionare.]]>
- Aggiornamento richiesto
- Installa la versione più recente di LSPosed
- Suggerimenti per lo sviluppatore del modulo
- Disattivare le ottimizzazioni di distribuzione su Android Studio, o utilizzare il comando `gradlew installDebug` per eseguire l\'installazione. Altrimenti l\'apk del modulo non verrà aggiornato.
- Versione API
- Versione del framework
- Nome pacchetto del manager
- Versione del sistema
- Dispositivo
- ABI del sistema
- Dex Optimizer Wrapper
- Abilitato
- Non abilitato
- Supportato
- Non supportato
- Versione di Android non soddisfatta
- Arrestato in modo anomalo
- Mount fallito
- SELinux è in modalità permissiva
- La politica di SELinux non è corretta
- Aggiorna LSPosed
- Confermi di voler aggiornare LSPosed? Il dispositivo verrà riavviato dopo il completamento dell\'aggiornamento
- Copiato negli appunti
-
- Benvenuto in LSPosed
- Stai usando il manager parassitario, che può creare scorciatoie o essere aperto dalla notifica.
- Stai usando il manager parassitario, che può essere aperto dalla notifica.
- Crea scorciatoia
- Non mostrare mai
- Manager parassitario consigliato
- LSPosed ora supporta la parassitazione del sistema per evitarne il rilevamento, è possibile aprire il manager parassitario dalla notifica. Si consiglia di disinstallare l\'applicazione attuale.
-
- Salva
- Log verbosi
- Log dei moduli
- Salvataggio log, attendere
- Log salvati
- Salvataggio non riuscito:\n%s
- Cancella il log ora
- Log cancellato con successo.
- Scorri in alto
- Caricamento in corso…
- Scorri in basso
- Ricarica
- Impossibile cancellare il log
- A capo automatico
- Log verboso abilitato
- Log verboso disabilitato
-
- (nessuna descrizione fornita)
- Questo modulo richiede una versione più recente di Xposed (%d) e quindi non può essere attivato
- Questo modulo è progettato per una versione più recente di Xposed (%d) e quindi alcune funzionalità potrebbero non funzionare
- Questo modulo non specifica la versione Xposed necessaria.
- Questo modulo è stato creato per la versione %1$d di Xposed ma, a causa di modifiche incompatibili nella versione %2$d, è stato disabilitato
- Questo modulo non può essere caricato perché è installato sulla scheda SD, spostalo nella memoria interna
- Disinstalla
- Impostazioni modulo
- Visualizza nel repository
- Vuoi disinstallare questo modulo?
- %1$s disinstallato
- Disinstallazione non riuscita
- Aggiungi modulo all\'utente
- %1$s aggiunto all\'utente %2$s
- Aggiunta del modulo fallita
- Installa per l\'utente %s
- Vuoi installare %1$s per l\'utente %2$s? Si consiglia di farlo manualmente, forzare l\'installazione da LSPosed potrebbe causare problemi.
- espandi
- comprimi
-
- Ri-ottimizza
- Ottimizzazione in corso…
- Ottimizzazione completata
- Avvia
- Ottimizzazione non riuscita: il valore restituito è vuoto
- Ottimizzazione fallita:
- Nome dell\'applicazione
- Nome del pacchetto
- Data di installazione
- Data di aggiornamento
- Inverso
- Applicazioni di sistema
- Ordina
- Abilita modulo
- Non hai selezionato nessuna app. Continuare?
- Giochi
- Moduli
- Impossibile salvare l\'elenco delle attivazioni
- Versione: %1$s
- Seleziona consigliate
- Non hai selezionato nessuna app. Selezionare le app consigliate?
- Selezionare le app consigliate?
- Il modulo Xposed non è ancora attivo
- Seleziona consigliate
- Aggiornamento disponibile: %1$s
- Il modulo %s è stato disabilitato poiché nessuna app è stata selezionata.
- Framework di sistema
- Backup
- Backup
- Ripristina
- Forza l\'arresto
- Forzare l\'arresto?
- Se forzi l\'interruzione di un\'app, potrebbe non funzionare correttamente.
- È necessario riavviare per applicare questa modifica
- Riavvia
- Nascondi
-
- Mostra in un\'altra app
- Informazioni app
- ¯\\\\_(ツ)_\\/¯\nNon c\'è nulla qui
-
- Framework
- Disabilita il log verboso
- La segnalazione di problemi richiede l\'inclusione di log dettagliati
- Tema nero scuro
- Usa il tema nero puro quando è abilitato il tema scuro
- Tema
- Backup e ripristino
- Backup dell\'elenco dei moduli e delle attivazioni.
- Ripristino dell\'elenco dei moduli e delle attivazioni.
- Backup
- Salvataggio non riuscito:\n%s
- Abilitare DocumentUI
- Ripristina
- Ripristino fallito:\n%s
- Rete
- DNS over HTTPS
- Aggira l\'avvelenamento della cache DNS in alcune nazioni
- Colore del tema
- Colore del tema del sistema
- Forza le app a mostrare le icone nel launcher
- A partire da Android 10, le app non possono più nascondere le loro icone nel launcher. Disabilita l\'opzione per disattivare questa funzionalità.
- Sistema
- Lingua
- Contributori alla traduzione
- Partecipa alla traduzione
- Aiutaci a tradurre %s nella tua lingua
- Crea una scorciatoia che può aprire il manager parassitario
- Scorciatoia fissata
- L\'attuale launcher predefinito non supporta le scorciatoie con i pin
- Notifica di stato
- Mostra una notifica che può aprire il manager parassitario
- Canale di aggiornamento
- Stabile
- Beta
- Nightly
-
- Leggimi
- Versioni
- Informazioni
- Pagina web
- Codice sorgente
- Collaboratori
- Risorse
- Apri nel browser
- Mostra le versioni precedenti
- Non ci sono altre versioni
- Impossibile caricare il repository dei moduli: %s
- Aggiornabili prima
- Installato
-
- - %d download
- - %d downloads
-
-
- Sakura
- Rosso
- Rosa
- Viola
- Viola scuro
- Indaco
- Blu
- Azzurro
- Ciano
- Verde acqua
- Verde
- Verde chiaro
- Lime
- Giallo
- Ambra
- Arancione
- Arancione scuro
- Marrone
- Blu grigio
-
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
deleted file mode 100644
index af8ff4d51..000000000
--- a/app/src/main/res/values-iw/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- סקירה כללית
- מודולים
-
- - %d מודול מופעל
- - %d מודולים מופעלים
- - %d מודולים מופעלים
- - %d מודולים מופעלים
-
- לוגים
- הגדרות
- משוב או הצעה
- אודות
- דווח על בעיה
- מאגר מידע
- כל המודולים מעודכנים
- פורסם ב-%s
- עודכן ב-%s
-
- - %d מודולים ניתנים לשדרוג
- - %d מודולים ניתנים לשדרוג
- - %d מודולים ניתנים לשדרוג
- - %d מודולים ניתנים לשדרוג
-
- הצטרף לערוץ %2$s שלנו]]>
- ריק
- התקנה
- הקש כדי להתקין את LSPosed
- לא מותקן
- LSPosed אינו מותקן
- הופעל
- הופעל חלקית
- SEPolicy לא נטען כהלכה
- נא לדווח על כך למפתחי Magisk .]]>
- הזרקת תשתית המערכת נכשלה
- Magisk או מודולי Magisk באיכות נמוכה. אנא נסה להשבית מודולי Magisk שאינם Riru ו-LSPosed או שלח דיווח לוג מלא למפתחים.]]>
- מאפיין המערכת שגוי
- המודולים עשויים להתבטל מדי פעם.]]>
- צריך לעדכן
- נא להתקין את הגרסה העדכנית ביותר של LSPosed
- גרסת API
- גרסת תשתית
- שם חבילת מנהל
- גרסת מערכת
- מכשיר
- מערכת ABI
- עטיפה של Dex Optimizer
- מופעל
- לא מופעל
- נתמך
- לא נתמך
- גרסת אנדרואיד לא מספקת
- התרסק
- ה-mount נכשל
- מדיניות SELinux הינה מתירנית
- מדיניות SELinux הינה שגויה
- עדכן LSPosed
- אשר לעדכן את LSPosed? מכשיר זה יאתחל לאחר השלמת העדכון
- הועתק ללוח
-
- ברוכים הבאים ל-LSPosed
- הנך משתמש בגרסת אפליקציית ניהול בתצורת טפיל, שיכולה ליצור קיצור דרך או עדיין להיפתח מהתראה.
- הנך משתמש בגרסת אפליקציית ניהול בתצורת טפיל, שיכולה להיפתח מהתראה.
- צור קיצור דרך
- לעולם אל תראה
- מומלצת גרסת אפליקציית ניהול בתצורת טפיל
- LSPosed תומך כעת בתצורת טפיל כדי למנוע זיהוי, ניתן לפתוח את גרסת אפליקציית הניהול בתצורת טפיל מהתראה. מומלץ להסיר את האפליקציה הנוכחית.
-
- שמור
- לוגים מפורטים
- לוגים של מודולים
- שומר יומן, אנא המתן
- לוגים נשמרו
- השמירה נכשלה:\n%s
- נקה לוגים עכשיו
- לוגים נוקו בהצלחה.
- גלול למעלה
- טוען…
- גלול למטה
- רענן
- נכשל בניקוי הלוגים
- עטיפת מילה
- לוגים מפורטים מופעלים
- לוגים מפורטים מושבתים
-
- (לא מסופק תיאור)
- מודול זה דורש גרסה חדשה יותר של LSPosed (%d) ולכן לא יכול להיות מופעל
- מודול זה מיועד לגרסה חדשה יותר של Xposed (%d) ולכן ייתכן שחלק מהפונקציונליות לא תופעל כהלכה
- מודול זה לא מציין את גסרת ה- LSPosed שהוא צריך.
- מודול זה נוצר בשביל LSPosed גרסה %1$d, אך בשל חוסר תאימות לאחור בגרסה %2$d, הוא הופסק
- מודול זה לא יכול להיטען מכיוון שהוא מותקן על כרטיס ה-SD, אנא העבר אותו לאחסון פנימי
- הסר התקנה
- הגדרות מודול
- הצג ברפוסיטורי
- האם אתה בטוח שאתה רוצב להסיר את התנקת המודול?
- %1$s הוסר
- הסרת ההתקנה הושלמה בהצלחה
- הוסף מודל למשתמש
- נוסף %1$s למשתמש %2$s
- הוספת המודל נכשלה
- התקן למשתמש %s
- רוצה להתקין %1$s למשתמש %2$s? מומלץ להתקין באופן ידני, כפיית התקנה באמצעות LSPosed עלולה לגרום לבעיות.
- להרחיב
- התמוטטות
-
- מטב מחדש
- ממטב…
- אופטימיזציה הושלמה.
- הרץ
- אופטימיזציה נכשלה או שהערך המוחזר הוא ריק.
- אופטימיזציה נכשלה:
- מיין על פי שם האפליקציה
- מיין על פי שם החבילה
- מיין על פי זמן ההתקנה
- מיין על פי זמן העדכון
- להפוך
- אפליקציות מערכת
- ממיין
- הפעל מודול
- אתה לא בחרת שום אפליקציה. להמשיך?
- משחקים
- מודולים
- נכשל לשמור רשימת תחומים
- גרסה: %1$s
- מומלץ
- אתה לא בחרת שום אפליקציה. לבחור אפליקציות מומלצות?
- בחר אפליקציות מומלצות?
- מודול LSPosed עדיין לא הופעל
- מומלץ
- עדכון זמין: %1$s
- מודול %s בוטל מכיוון שלא נבחרה שום אפליקציה.
- מערכת Framework
- מגבה
- גיבוי
- שחזור
- אלץ עצירה
- אצץ עצירה?
- אם אתה תאלץ עצירה לאפליקציה, היא עלולה להתנהג בצורה לא רצויה.
- הפעלה מחדש דרושה בכדי שהשינויים יכנסו לתוקף
- הפעל מחדש
- הסתר
-
- הצג באפליקציה אחרת
- מידע על האפליקציה
- ¯\\\\_(ツ)_\/¯\n אין פה כלום
-
- Framework
- בטל verbose logs
- בקשת דיווח על בעיות לכלול יומנים מילוליים
- ערכת נושא שחור כהה
- השתמש בערכת נושא שחור טהור אם ערכת נושא כהה מופעלת
- ערכת נושא
- גיבוי ושחזור
- גיבוי רשימת מודולים ורשימות היקף.
- שחזר את רשימת המודולים ורשימות ההיקף.
- גיבוי
- גיבוי נכשל:\n%s
- אנא הפעל את DocumentUI
- שחזור
- השחזור נכשל::\n%s
- רשת
- DNS על פני HTTPS
- מעקף הרעלת DNS במדינות מסוימות
- צבע ערכת נושא
- צבע נושא המערכת
- כפה על אפליקציות להציג סמלי מפעיל
- לאחר Android 10, אפליקציות אינן מורשות להסתיר את סמלי המשגר שלהן. בטל את הלחצן הדו-מצבי כדי להפוך תכונת מערכת זו ללא זמינה.
- מערכת
- שפה
- תורמים לתרגום
- השתתף בתרגום
- עזור לנו לתרגם את %s לשפה שלך
- צור קיצור דרך שיכול לפתוח מנהל טפילי
- קיצור דרך הוצמד
- מפעיל ברירת המחדל הנוכחי אינו תומך בקיצורי דרך
- הודעת סטטוס
- הצג הודעה שיכולה לפתוח מנהל טפילי
- ערוץ עדכון
- יציב
- בטא
- בניה לילית
-
- קרא אותי
- גרסאות
- מידע
- דף בית
- קוד מקור
- מפתחים
- קבצים
- פתח בדפדפן
- הראה גרסאות ישנות
- אין עוד גרסאות
- נשכל לטעון מודול: %s
- ניתן לשדרוג תחילה
- מוּתקָן
-
- - %d הורדה
- - %d הורדות
- - %d הורדות
- - %d הורדות
-
-
- סאקורה
- אדום
- ורוד
- סגול
- סגול עמוק
- אינדיגו
- כחול
- כחול בהיר
- טורקיז
- ירוק כחלחל
- ירוק
- ירוק בהיר
- לימון
- צהוב
- ענבר
- כתום
- כתום עמוק
- חום
- כחול אפור
-
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
deleted file mode 100644
index 6a1a99fc7..000000000
--- a/app/src/main/res/values-ja/strings.xml
+++ /dev/null
@@ -1,239 +0,0 @@
-
-
-
-
- 概要
- モジュール
-
- - %d 個のモジュールが有効です
-
- ログ
- 設定
- フィードバックまたは提案
- このアプリについて
- 問題を報告
- リポジトリ
- 全てのモジュールが最新です
- 公開日時: %s
- 更新日時: %s
-
- - %d 個のモジュールが更新可能です
-
- チャンネルに参加するにはこちら: %2$s]]>
- yoshi818, a1678991, りお(koko0628)
- インストール
- タップして LSPosed をインストールします
- インストールされていません
- LSPosed はインストールされていません
- 有効化済み
- 部分的に有効化済み
- SEPolicy が正しく読み込まれていません
- これをMagisk の開発者に報告してください。]]>
- システムフレームワークへのパッチに失敗しました
- Magiskまたは低品質のMagiskモジュールが原因である可能性があります。 LSPosed以外のMagiskモジュールを一時的に無効化してみるか、完全なログを開発者に送信してください。]]>
- システム設定が正しくありません
- モジュールが無効化される場合があります。]]>
- 更新が必要です
- 最新バージョンの LSPosed をインストールして下さい
- モジュール開発者向けのヒント
- Android Studio でデプロイの最適化を無効にするか、「gradlew installDebug」コマンドを使用してインストールしてください。この操作を行わないとモジュールの apk は更新できません。
- API バージョン
- フレームワークバージョン
- マネージャーパッケージ名
- システムバージョン
- デバイス
- システム ABI
- Dex 最適化ラッパー
- 有効
- 無効
- 対応
- 非対応
- Android のバージョンが適合しません
- クラッシュしました
- マウントに失敗しました
- SELinux は Permissive です
- SELinux のポリシーが正しくありません
- LSPosed を更新
- LSPosed を更新してもよろしいですか?更新の完了後、このデバイスは再起動します
- クリップボードにコピーされました
-
- LSPosed へようこそ
- パラサイトマネージャーを使用しています。これにより、ショートカットを作成したり、通知から開いたりできます。
- 通知から開くことができるパラサイトマネージャーを使用しています。
- ショートカットを作成
- 再度表示しない
- パラサイトマネージャーを使用することを推奨します
- LSPosed は検出を回避するためのシステムパラサイトに対応し、通知からパラサイトマネージャーを開くことができるようになりました。現在のアプリケーションをアンインストールすることをおすすめします。
-
- 保存
- 詳細ログ
- モジュールログ
- ログを保存しています。お待ちください
- ログを保存しました
- 保存に失敗しました:\n%s
- ログを消去
- ログの消去に成功しました。
- ログの先頭行にスクロール
- 読み込み中…
- 一番下までスクロール
- 再読み込み
- ログを消去できませんでした
- 単語を折り返す
- 詳細ログは有効です
- 詳細ログは無効です
-
- (説明はありません)
- このモジュールは新しいバージョンの Xposed (%d) が必要なので有効化できません
- このモジュールは新しい Xposed バージョン (%d) 用に設計されているため、一部の機能が動作しない可能性があります
- このモジュールは必要な Xposed のバージョンを指定していません。
- このモジュールは Xposed バージョン %1$d 用に作成されましたが、バージョン %2$dでの互換性のない変更により無効化されました。
- このモジュールは SD カードにインストールされているため読み込むことができません。内部ストレージに移動してください。
- アンインストール
- モジュール設定
- リポジトリで表示
- このモジュールをアンインストールしますか?
- 「%1$s」をアンインインストールしました
- アンインストールに成功
- ユーザへモジュールを追加
- %1$s をユーザー %2$s へ追加
- モジュールの追加に失敗しました
- ユーザー %s へインストール
- ユーザー %2$s へ %1$s をインストールしますか?LSPosed 経由での強制インストールは問題が発生する場合があるため、手動でインストールすることをおすすめします。
- 開く
- 閉じる
-
- 再最適化
- 最適化中…
- 最適化が完了しました
- 起動
- 最適化に失敗しました: 戻り値が空です
- 最適化に失敗しました:
- アプリ名
- パッケージ名
- インストール日時
- 更新日時
- 逆順
- システムアプリ
- 並び順
- モジュールを有効化
- アプリが選択されていません。よろしいですか?
- ゲーム
- モジュール
- スコープリストの保存に失敗しました
- バージョン: %1$s
- 選択
- 推奨
- アプリが選択されていません。おすすめのアプリを選択しますか?
- おすすめのアプリを選択しますか?
- すべて
- なし
- 自動的に含む
- Xposed モジュールが有効化されていません
- 推奨
- アップデートが利用可能です: %1$s
- アプリが選択されていないため、モジュール「%s」は無効になっています。
- システムフレームワーク
- バックアップ
- バックアップ
- 復元
- 強制停止
- 強制停止しますか?
- アプリを強制停止すると、アプリが動作しなくなる可能性があります。
- この設定を適用するには再起動が必要です
- 再起動
- 非表示
-
- 他のアプリで表示
- アプリの情報
- ¯\\_(ツ)_\/¯\nリストは空です
-
- フレームワーク
- 詳細ログの無効化
- 問題を報告する際は、詳細なログを含めるようにしてください
- 黒のダークテーマ
- ダークテーマが有効になっている場合は、ピュアブラックテーマを使用します
- テーマ
- バックアップと復元
- モジュールリストとスコープリストをバックアップします
- モジュールリストとスコープリストを復元します
- バックアップ
- バックアップに失敗しました:\n%s
- DocumentUI を有効にしてください
- 復元
- 復元に失敗しました:\n%s
- ネットワーク
- DNS over HTTPS
- 一部の国向けの DNS ポイズニング回避策
- テーマカラー
- システムテーマカラー
- ランチャーアイコンを強制的に表示
- Android 10 以降、アプリはランチャーアイコンを隠すことができなくなりました。このシステム機能を無効にするには、トグルをオフにしてください。
- システム
- 言語
- 翻訳貢献者
- 翻訳に貢献
- %s の翻訳にご協力ください
- パラサイトマネージャーを開くことができるショートカットを作成します
- ショートカットのピン留め
- 現在のデフォルトランチャーはショートカットのピン留めをサポートしていません
- ステータス通知
- パラサイトマネージャーを開くことができる通知を表示します
- 更新チャンネル
- 安定版
- ベータ版
- ナイトリービルド
-
- Readme
- リリース
- 情報
- ホームページ
- ソースコード
- 協力者
- アセット
- ブラウザで表示
- 以前のバージョンを表示
- これ以上のリリースはありません
- モジュールリポジトリの読み込みに失敗しました: %s
- 更新があるモジュールを先頭に表示
- インストール済み
-
- - %d 件のダウンロード
-
-
- 桜色
- 赤
- ピンク
- 紫
- 深紫色
- 藍色
- 青
- 水色
- シアン
- 青緑
- 緑
- ライトグリーン
- ライム
- 黄色
- アンバー
- オレンジ色
- ディープオレンジ
- 茶色
- ブルーグレー
-
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
deleted file mode 100644
index 241e44cca..000000000
--- a/app/src/main/res/values-ko/strings.xml
+++ /dev/null
@@ -1,235 +0,0 @@
-
-
-
-
- 개요
- 모듈
-
- - 모듈 %d개가 활성화됨
-
- 로그
- 설정
- 피드백 혹은 제안
- 정보
- 문제 보고
- 저장소
- 모든 모듈이 최신 버전입니다
- %s 에 배포됨
- %s에 업데이트됨
-
- - %d개의 모듈이 업데이트 가능합니다
-
- %2$s 채널에 가입하세요]]>
- green1052
- 설치
- LSPosed를 설치하려면 누르세요
- 설치되지 않음
- LSPosed가 설치되지 않았습니다
- 활성화됨
- 부분적으로 활성화됨
- SEPolicy가 제대로 로드되지 않았습니다
- 오류를 Magisk 개발자 에게 신고해주세요]]>
- System Framework 삽입 실패
- Magisk 또는 일부 Magisk 모듈 때문일 수 있습니다. Riru 및 LSPosed 이외의 Magisk 모듈을 비활성화 하거나 전체 로그를 개발자에게 제출하세요.]]>
- 시스템 속성이 잘못됨
- 모듈은 가끔 무효화될 수 있습니다.]]>
- 업데이트가 필요합니다
- 최신 버전으로 LSPosed를 설치해 주세요
- 모듈 개발자를 위한 팁
- Android Studio 에서 배포 최적화를 비활성화 하거나, 설치할때 \'gradlew installDebug\' 명령어를 사용해주세요. 그렇지 않으면 모듈 APK 가 업데이트되지 않을 것입니다.
- API 버전
- Framework 버전
- 관리자 패키지 이름
- System 버전
- 장치
- 시스템 ABI
- Dex 옵티마이저 래퍼
- 사용
- 사용 안 함
- 지원
- 지원되지 않음
- 안드로이드 버전이 지원되지 않습니다
- 충돌
- 마운트 실패
- SELinux가 허용됩니다
- SELinux 정책이 잘못되었습니다
- LSPosed 업데이트
- LSPosed를 업데이트 하시겠습니까? 업데이트 완료 후에 재부팅합니다
- 클립보드에 복사됨
-
- LSPosed에 오신 것을 환영합니다
- 바로 가기를 만들거나 알림에서 계속 열 수 있는 기생 관리자를 사용하고 있습니다.
- 알림에서 열 수 있는 내부 관리자를 사용 중 입니다.
- 바로 가기 만들기
- 표시 안 함
- 기생 매니저 추천
- LSPosed는 이제 탐지를 피하기 위해 시스템 기생을 지원하며 알림에서 기생 관리자를 열 수 있습니다. 현재 응용 프로그램을 제거하는 것이 좋습니다.
-
- 저장
- 자세한 로그
- 모듈 로그
- 로그를 저장중입니다, 기다려주세요
- 로그가 저장되었습니다
- 저장 실패:\n%s
- 로그 지우기
- 로그를 성공적으로 지웠습니다.
- 맨 위로 스크롤
- 로딩 중…
- 아래로 스크롤
- 리로드
- 로그를 지우지 못했습니다.
- 줄 바꿈
- 자세한 로그 사용
- 자세한 로그 비활성화
-
- (설명 없음)
- 이 모듈에는 최신 LSPosed (%d) 버전이 필요하므로 활성화할 수 없습니다.
- 이 모듈은 더 새로운 Xposed 버전 (%d) 에서 만들어졌고 따라서 몇몇 기능들이 작동하지 않을 수도 있습니다
- 이 모듈에서는 필요한 LSPosed 버전을 지정하지 않습니다.
- 이 모듈은 LSPosed 버전 %1$d에 대해 생성되었지만 버전 %2$d에서 호환되지 않는 변경으로 인해 비활성화되었습니다.
- 이 모듈은 SD 카드에 설치되어 있으므로 로드할 수 없습니다. 내부 스토리지로 이동하십시오.
- 제거
- 모듈 설정
- 저장소에서 보기
- 이 모듈을 제거하시겠습니까?
- %1$s 제거됨
- 제거 실패
- 사용자에게 모듈 추가
- %2$s 사용자에 %1$s 추가
- 모듈 추가 실패
- %s 사용자에게 설치
- %2$s 사용자에게 %1$s을(를) 설치하시겠습니까? 수동으로 설치하는 것이 좋습니다. LSPosed를 통해 강제로 설치하면 문제가 발생할 수 있습니다.
- 펼치기
- 접기
-
- 다시 최적화
- 최적화 중…
- 최적화 완료
- 실행
- 최적화에 실패했거나 반환 값이 비어 있습니다.
- 최적화 실패:
- 애플리케이션 이름
- 패키지 이름
- 설치 시간
- 업데이트 시간
- 역순
- 시스템 앱
- 정렬
- 모듈 활성화
- 앱을 선택하지 않았습니다. 계속하시겠습니까?
- 게임
- 모듈
- 범위 목록 저장에 실패했습니다.
- 버전: %1$s
- 권장
- 앱을 선택하지 않았습니다. 권장 앱을 선택하시겠습니까?
- 권장 앱을 선택하시겠습니까?
- LSPosed 모듈이 아직 활성화되지 않았습니다.
- 권장
- 업데이트 가능: %1$s
- 앱을 선택하지 않았기 때문에 %s 모듈이 비활성화되었습니다.
- 시스템 프레임워크
- 백업
- 백업
- 복원
- 강제 중지
- 강제 중지?
- 앱을 강제로 중지하면 잘못된 동작이 발생할 수 있습니다.
- 이 변경 내용을 적용하려면 재부팅해야 합니다.
- 재부팅
- 숨김
-
- 다른 앱으로 보기
- 앱 정보
- ¯\\\\_(ツ)_\/¯\n아무것도 없음
-
- 프레임워크
- 상세한 로그 비활성화
- 자세한 로그를 포함한 오류 신고 요청
- 블랙 다크 테마
- 다크 테마가 활성화된 경우 순수 검은색 테마를 사용합니다.
- 테마
- 백업 및 복원
- 모듈 목록과 스코프 목록을 백업합니다.
- 모듈 목록과 스코프 목록을 복원합니다.
- 백업
- 백업 실패:\n%s
- DocumentUI를 활성화하십시오
- 복원
- 복원 실패:\n%s
- 네트워크
- DNS over HTTPS
- 일부 국가의 DNS 포이즈닝 문제를 해결합니다
- 테마 색
- System 강조 색
- 앱에서 시작 프로그램 아이콘 표시
- Android 10 이후 앱(특히 Xposed 모듈)은 시작 프로그램 아이콘을 숨길 수 없습니다. 이 기능을 비활성화하려면 토글을 끄십시오.
- 체계
- 언어
- 번역 기여자
- 번역 참여
- %s를 귀하의 언어로 번역하는 데 도움을 주세요.
- 내부 관리자를 열 수 있는 바로가기 생성
- 바로 가기 고정
- 현재 기본 런쳐는 핀 바로가기를 지원하지 않습니다
- 상태 알림
- 기생 관리자를 열 수 있는 알림 표시
- 업데이트 채널
- 안정
- 베타
- 야간 빌드
-
- 읽어보기
- 릴리스
- 정보
- 홈페이지
- 소스 코드
- 기여자
- 자산
- 브라우저에서 열기
- 이전 버전 표시
- 더 이상 출시되지 않음
- 모듈 저장소를 로드하지 못함: %s
- 먼저 업그레이드 가능
- 설치됨
-
- - %d 다운로드
-
-
- 벚꽃
- 빨강
- 분홍
- 보라
- 짙은 보라
- 군청
- 파랑
- 밝은 파랑
- 청록
- 암청
- 초록
- 연두
- 라임
- 노랑
- 호박
- 주황색
- 짙은 주황
- 갈색
- 청회색
-
diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml
deleted file mode 100644
index d116177ae..000000000
--- a/app/src/main/res/values-ku/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- کورتەی گشتی
- مۆدیوولەکان
-
- - %d مۆدیول چالاک کراوە
- - %d مۆدیول چالاک کراوە
-
- لۆگەکان
- ڕێکخستنەکان
- فیدباک یان پێشنیار
- دەربارە
- پرسی ڕاپۆرت
- کۆگا
- Hemî modulên nûjen
- Di %sde hate weşandin
- Di %sde hate nûve kirin
-
- - %d module nûvekirin
- - %d modulên nûvekirî
-
- bibînin Tevlî %2$s kanala me bibin]]>
- null
- Lêkirin
- Ji bo sazkirina LSPosed bikirtînin
- Ne hatiye sazkirin
- LSPosed nayê Sazkirin
- Çalak kirin
- Qismî aktîf kirin
- SEPolicy bi rêkûpêk nayê barkirin
- Ji kerema xwe vê yekê ji Magisk pêşdebir re ragihînin.]]>
- Derzkirina Çarçoveya Pergalê têk çû
- Magisk an hin modulên Magisk-a kêm-kalîteyê ve çêbibe. Ji kerema xwe hewl bidin ku modulên Magisk ji bilî Riru û LSPosed neçalak bikin an jî têketinek tevahî ji pêşdebiran re bişînin.]]>
- Pêşniyara pergalê xelet e
- Dibe ku modul carinan betal bibin.]]>
- Pêdivî ye ku nûve bike
- Ji kerema xwe guhertoya herî dawî ya LSPosed saz bikin
- Guhertoya API
- Guhertoya çarçoveyê
- Navê pakêta rêveberê
- Guhertoya pergalê
- Sazî
- Pergala ABI
- Dex Optimizer Wrapper
- Enabled
- Ne çalak kirin
- Piştgirî kirin
- Piştgirî nekirin
- Guhertoya Android-ê ne razî ye
- Qeza kirin
- Çiya têk çû
- SELinux destûr e
- Siyaseta SELinux nerast e
- LSPosed nûve bikin
- Piştrast bike ku LSPosed nûve bike? Ev cîhaz dê piştî qedandina nûvekirinê ji nû ve dest pê bike
- Li clipboardê hate kopî kirin
-
- Hûn bi xêr hatin LSPosed
- Hûn rêveberê parazît bikar tînin, ku dikare kurtebirê biafirîne an hîn jî ji ragihandinê vebe.
- Hûn rêveberê parazît bikar tînin, ku dikare ji ragihandinê vebe.
- Kurtenivîsê çêbikin
- Qet nîşan nedin
- Rêvebirê Parazît Pêşniyar kirin
- LSPosed naha parazîtkirina pergalê piştgirî dike da ku ji tespîtê dûr bixe, hûn dikarin rêveberê parazît ji ragihandinê vekin. Tê pêşniyar kirin ku serîlêdana heyî jêbirin.
-
- Rizgarkirin
- Têketinên Verbose
- Têketinên Modulan
- پاشەکەوتکردنی لۆگ، تکایە چاوەڕوان بن
- Têketin xilas kirin
- Sazkirin bi ser neket:\n%s
- Naha têketinê paqij bike
- Têketin bi serkeftî hate paqij kirin.
- Scroll to top
- Barkirin…
- Scroll to bottom
- Ji nû ve barkirin
- Paqijkirina têketinê bi ser neket
- Peyv Wrap
- Têketinê bi lêker vekir
- Têketinê bi devkî neçalak bû
-
- (bê şirove nehat dayîn)
- Vê modulê guhertoyek nû ya Xposed (%d) hewce dike û ji ber vê yekê nayê çalak kirin
- Ev modul ji bo guhertoyek nû ya Xposed (%d) hatî çêkirin û ji ber vê yekê dibe ku hin fonksiyon nexebitin
- Ev modul guhertoya Xposed ya ku jê re hewce dike diyar nake.
- Ev modul ji bo guhertoya Xposed %1$dhate afirandin, lê ji ber guheztinên nelihev ên di guhertoya %2$d-ê de, ew hate asteng kirin.
- Ev modul nikare were barkirin ji ber ku ew li ser qerta SD-ê hatî saz kirin, ji kerema xwe wê biguhezînin hilana hundurîn
- Rakirin
- Mîhengên Modulê
- Di Repo de bibînin
- Ma hûn dixwazin vê modulê rakin?
- Rakir %1$s
- Rakirina neserketî ye
- Modulê li bikarhênerê zêde bikin
- %1$s ji bikarhêner %2$sre zêde kirin
- Zêdekirina modulê têk çû
- Ji bikarhêner %sre saz bike
- Dixwazin %1$s ji bikarhêner %2$sre saz bikin? Tête pêşniyar kirin ku bi destan saz bikin, zordariya sazkirinê bi riya LSPosed dibe ku bibe sedema pirsgirêkan.
- firehkirin
- jiberhevketin
-
- Ji nû ve xweşbîn bikin
- Optimîzekirin…
- Optimîzasyon qediya
- Dest pê bike
- Optimîzasyon têk çû: nirxa vegerê vala ye
- Optimîzasyon têk çû:
- Navê serîlêdanê
- Navê pakêtê
- Dema sazkirinê
- Wextê nûve bike
- Gara paşî
- Sepanên pergalê
- Rêzkirin
- Modulê çalak bike
- Te tu sepanê hilnebijart. Berdewamkirin?
- Games
- Modules
- Hilbijartina navnîşa çarçovê bi ser neket
- Versiyon: %1$s
- Pêşniyar kirin
- Te tu sepanê hilnebijart. Serlêdanên pêşniyarkirî hilbijêrin?
- Serlêdanên pêşniyarkirî hilbijêrin?
- Modula Xposed hîn nehatiye çalak kirin
- Pêşniyar kirin
- Nûvekirin heye: %1$s
- Modula %s ji ber ku tu sepan nehat hilbijartî hate neçalak kirin.
- Çarçoveya Sîstemê
- Backup
- Backup
- Nûvdekirin
- Bi zorê rawestandin
- Rawestandina zorê?
- Ger hûn bi zorê sepanek rawestînin, dibe ku ew xelet tevbigere.
- Ji bo sepandina vê guherînê ji nû ve destpêkirinê hewce ye
- Reboot
- Veşartin
-
- Di sepana din de bibînin
- Agahdariya Appê
- ¯\\\\_(ツ)_\/¯\nLi vir tiştek tune
-
- Çarçove
- Têketinên devkî neçalak bike
- Daxwaza pirsgirêkan rapor bikin ku têketinên devkî tê de bin
- Mijara reş reş
- Ger mijara tarî çalak be, mijara reş a paqij bikar bînin
- Mijad
- Backup û restore
- Lîsteya modulê paşvekêşîn û navnîşên çarçovê.
- Lîsteya modul û navnîşên çarçovê vegerînin.
- Backup
- Piştgiriya bi ser neket:\n%s
- Ji kerema xwe DocumentUI çalak bike
- Nûvdekirin
- Vegerandin bi ser neket:\n%s
- Network
- DNS li ser HTTPS
- Li hin welatan jehrîkirina DNS-ê çareser bikin
- Rengê mijarê
- Rengê mijara pergalê
- Bi zorê serlêdanan bikin ku îkonên destpêker nîşan bidin
- Piştî Android 10, serîlêdan destûr nayê dayîn ku îkonên xwe yên destpêker veşêrin. Ji bo neçalakkirina vê taybetmendiya pergalê, guheztinê vekin.
- Sîstem
- Ziman
- Beşdarên wergerê
- Beşdarî wergerê bibin
- Alîkariya me bikin ku %s wergerînin zimanê we
- Kurtenivîsek çêbikin ku dikare rêveberê parazît veke
- Kurtenivîs pêçayî
- Destpêkera xwerû ya heyî kurtebirên pin piştgirî nake
- Notification Status
- Agahdariyek nîşan bide ku dikare rêveberê parazît veke
- Kanalê nûve bikin
- Stewr
- Beta
- Avakirina şevê
-
- Readme
- Releases
- Info
- Homepage
- Koda çavkaniyê
- Hevkar
- Tiştan
- Di gerokê de vekin
- Guhertoyên kevntir nîşan bide
- Bêtir berdan
- Barkirina depoya modulê têk çû: %s
- Pêşî nûvekirin
- Saz kirin
-
- - %d download
- - %d downloads
-
-
- Sakura
- sor
- Pembe
- Mor
- binefşî ya kûr
- Indigo
- Şîn
- Şînê vebûyî
- Cyan
- Teal
- Kesk
- Kesk ronî
- Lime
- Zer
- Aqût
- porteqalî
- porteqala kûr
- qehweyî
- Şîn gewr
-
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
deleted file mode 100644
index 63500a800..000000000
--- a/app/src/main/res/values-lt/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- Apžvalga
- Moduliai
-
- - %d modulis įjungtas
- - %d įjungti moduliai
- - %d įjungti moduliai
- - %d įjungti moduliai
-
- Žurnalai
- Nustatymai
- Atsiliepimai arba pasiūlymas
- About-face
- Pranešti apie problemą
- Saugykla
- Visi moduliai atnaujinti
- Paskelbta adresu %s
- Atnaujinta %s
-
- - %d modulis atnaujinamas
- - %d atnaujinami moduliai
- - %d atnaujinami moduliai
- - %d atnaujinami moduliai
-
- Prisijunkite prie mūsų %2$s kanalo]]>
- Ace Miller, im sorry
- Įdiekite
- Bakstelėkite, jei norite įdiegti LSPosed
- Neįdiegta
- \"LSPosed\" nėra įdiegta
- Aktyvuota
- Iš dalies aktyvuota
- \"SEPolicy\" nėra tinkamai įkelta
- Apie tai praneškite Magisk kūrėjui.]]>
- Nepavyko įšvirkšti sistemos pagrindo
- "Magisk" arba kai kurių nekokybiškų "Magisk" modulių. Pabandykite išjungti kitus "Magisk" modulius, išskyrus "Riru" ir "LSPosed", arba pateikite visą žurnalą kūrėjams.]]>
- Neteisingas sistemos rekvizitas
- Moduliai kartais gali būti pripažinti negaliojančiais.]]>
- Reikia atnaujinti
- Įdiekite naujausią \"LSPosed\" versiją
- API versija
- Pagrindų versija
- Valdytojo paketo pavadinimas
- Sistemos versija
- Įrenginys
- Sistemos ABI
- \"Dex Optimizer Wrapper
- Įjungta
- Neįjungta
- Palaikomas
- Nepalaikomas
- \"Android\" versija nepatenkinti
- Sugedo
- Montavimas nepavyko
- \"SELinux\" yra leidžiamoji
- \"SELinux\" politika yra neteisinga
- Atnaujinti LSPosed
- Patvirtinkite, kad atnaujintumėte LSPosed? Baigus atnaujinimą šis prietaisas bus perkrautas
- Nukopijuota į iškarpinę
-
- Sveiki atvykę į LSPosed
- Jūs naudojate parazitinį tvarkytuvą, kuris gali sukurti nuorodą arba vis dar atidaryti iš pranešimo.
- Naudojate parazitinį tvarkytuvą, kuris gali būti atidarytas iš pranešimo.
- Sukurti nuorodą
- Niekada nerodykite
- Rekomenduojama parazitinė vadybininkė
- \"LSPosed\" dabar palaiko sistemos parazitavimą, kad išvengtų aptikimo, galite atidaryti parazitų tvarkyklę iš pranešimo. Rekomenduojama pašalinti dabartinę programą.
-
- Išsaugoti
- Žurnalai su verbaline informacija
- Modulių žurnalai
- Įrašomas žurnalas, palaukite
- Išsaugoti žurnalai
- Nepavyko išsaugoti:\n%s
- Išvalyti žurnalą dabar
- Žurnalas sėkmingai išvalytas.
- Slinkti į viršų
- Įkrovimas…
- Slinkite į apačią
- Perkrauti
- Nepavyko išvalyti žurnalo
- Žodžių apvyniojimas
- Įjungtas verstinis žurnalas
- Išjungtas verstinis žurnalas
-
- (aprašymas nepateiktas)
- Šis modulis reikalauja naujesnės \"Xposed\" versijos (%d), todėl negali būti aktyvuotas
- Šis modulis skirtas naujesnei \"Xposed\" versijai (%d), todėl kai kurios funkcijos gali neveikti.
- Šis modulis nenurodo jam reikalingos \"Xposed\" versijos.
- Šis modulis buvo sukurtas \"Xposed\" versijai %1$d, tačiau dėl nesuderinamų pakeitimų versijoje %2$d, jis buvo išjungtas.
- Šio modulio negalima įkelti, nes jis įdiegtas SD kortelėje, perkelkite jį į vidinę saugyklą
- Pašalinti
- Modulio nustatymai
- Peržiūrėti Repo
- Ar norite pašalinti šį modulį?
- Išmontuota %1$s
- Nesėkmingas pašalinimas
- Pridėti modulį prie naudotojo
- Pridėta %1$s naudotojui %2$s
- Nepavyko pridėti modulio
- Įdiegti naudotojui %s
- Norite įdiegti %1$s naudotojui %2$s? Rekomenduojama įdiegti rankiniu būdu, priverstinis diegimas per LSPosed gali sukelti problemų.
- išplėsti
- žlugimas
-
- Iš naujo optimizuoti
- Optimizavimas…
- Optimizavimas baigtas
- Paleiskite jį
- Optimizavimas nepavyko: grąžinama vertė yra tuščia
- Optimizavimas nepavyko:
- Programos pavadinimas
- Paketo pavadinimas
- Diegimo laikas
- Atnaujinimo laikas
- Atvirkštinis
- Sistemos programos
- Rūšiavimas
- Įjungti modulį
- Nepasirinkote jokios programos. Tęsti?
- Žaidimai
- Moduliai
- Nepavyko išsaugoti srities sąrašo
- Versija: %1$s
- Rekomenduojama
- Nepasirinkote jokios programos. Pasirinkti rekomenduojamas programas?
- Pasirinkite rekomenduojamas programas?
- Xposed modulis dar nėra aktyvuotas
- Rekomenduojama
- Galimas atnaujinimas: %1$s
- Modulis %s buvo išjungtas, nes nebuvo pasirinkta jokia programa.
- Sistemos sistema
- Atsarginė kopija
- Atsarginė kopija
- Atkurti
- Priverstinis sustabdymas
- Priverstinis sustojimas?
- Jei priverstinai sustabdysite programą, ji gali elgtis netinkamai.
- Kad šis pakeitimas būtų pritaikytas, reikia perkrauti kompiuterį
- Perkraukite
- Paslėpti
-
- Peržiūrėti kitoje programoje
- Programėlės informacija
- \\\\_(ツ)_\/¯\nČia nieko nėra
-
- Sistema
- Išjungti verbalinius žurnalus
- Ataskaitos klausimų prašymas įtraukti verbalinius žurnalus
- Juoda tamsi tema
- Naudokite grynai juodą temą, jei įjungta tamsi tema
- Tema
- Atsarginės kopijos kūrimas ir atkūrimas
- Atsarginės kopijos modulių ir sričių sąrašai.
- Atkurti modulių ir sričių sąrašus.
- Atsarginė kopija
- Nepavyko sukurti atsarginės kopijos:\n%s
- Įjunkite DocumentUI
- Atkurti
- Nepavyko atkurti:\n%s
- Tinklas
- DNS per HTTPS
- Apėjimas DNS apsinuodijimas kai kuriose šalyse
- Temos spalva
- Sistemos temos spalva
- Priversti programas rodyti paleidiklio piktogramas
- Po \"Android 10\" programėlėms neleidžiama slėpti paleidimo programos piktogramų. Norėdami išjungti šią sistemos funkciją, išjunkite perjungiklį.
- Sistema
- Kalba
- Vertimo paslaugų teikėjai
- Dalyvaukite vertimo procese
- Padėkite mums išversti %s į savo kalbą
- Sukurti nuorodą, kuri gali atidaryti parazitinį tvarkytuvą
- Prisegtas trumpinys
- Dabartinė numatytoji paleidimo programa nepalaiko prisegamų nuorodų
- Pranešimas apie būseną
- Rodyti pranešimą, kad galima atidaryti parazitinį tvarkytuvą
- Atnaujinti kanalą
- Stabilus
- Beta
- Naktinis kūrimas
-
- Readme
- Leidiniai
- Informacija
- Pradžia
- Šaltinio kodas
- Bendradarbiai
- Turtas
- Atidaryti naršyklėje
- Rodyti senesnes versijas
- Jokio išleidimo
- Nepavyko įkelti modulio atramos: %s
- Pirmiausia atnaujinamas
- Įdiegta
-
- - %d atsisiųsti
- - %d Parsisiųsti
- - %d Parsisiųsti
- - %d Parsisiųsti
-
-
- Sakura
- Raudona
- Rožinis
- Violetinė
- Tamsiai violetinė
- Indigo
- Mėlyna
- Šviesiai mėlyna
- Cyan
- Teal
- Žalioji
- Šviesiai žalia
- Lime
- Geltona
- Amber
- Oranžinė
- Tamsiai oranžinė
- Ruda
- Mėlyna pilka
-
diff --git a/app/src/main/res/values-night-v31/colors.xml b/app/src/main/res/values-night-v31/colors.xml
deleted file mode 100644
index 894a37a07..000000000
--- a/app/src/main/res/values-night-v31/colors.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- @android:color/system_accent1_800
- @android:color/system_accent1_200
-
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
deleted file mode 100644
index 6c4499226..000000000
--- a/app/src/main/res/values-night/colors.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- @color/abc_primary_text_material_light
- @color/abc_primary_text_material_dark
-
- #F06292
- #E1F5FE
-
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
deleted file mode 100644
index e36ab278d..000000000
--- a/app/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
deleted file mode 100644
index 341933fa6..000000000
--- a/app/src/main/res/values-nl/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Overzicht
- Modules
-
- - %d module ingeschakeld
- - %d modules ingeschakeld
-
- Logboeken
- Instellingen
- Feedback en suggestie
- Over
- Probleem melden
- Opslagplaats
- Alle modules zijn up-to-date
- Gepubliceerd op %s
- Bijgewerkt op %s
-
- - %d module te upgraden
- - %d modules te upgraden
-
- Sluit je aan bij onze %2$s kanaal]]>
- Pandemic-XDA
- Installeren
- Tik om LSPosed te installeren
- Niet geïnstalleerd
- LSPosed is niet geinstalleerd
- Geactiveerd
- Gedeeltelijk geactiveerd
- SEPolicy is niet correct geladen
- Gelieve dit te melden aan Magisk ontwikkelaar.]]>
- Systeem Framework injectie is mislukt
- Dit is zeldzaam en kan worden veroorzaakt door Magisk of sommige Magisk-modules van lage kwaliteit. Probeer andere Magisk-modules dan Riru en LSPosed uit te schakelen of stuur een volledig logboek naar de ontwikkelaars.]]>
- Systeem prop incorrect
- Modules kunnen af en toe ongeldig worden.]]>
- Bijwerken is nodig
- Installeer de laatste versie van LSPosed
- API versie
- Framework versie
- Naam beheerderspakket
- Systeem versie
- Apparaat
- Systeem ABI
- Dex Optimizer Wrapper
- Ingeschakeld
- Niet ingeschakeld
- Ondersteund
- Niet-ondersteunde
- Android versie onbevredigd
- Crashed
- Berg mislukt
- SELinux is permissief
- SELinux beleid is onjuist
- Update LSPosed
- Bevestig bijwerken van LSPosed? Dit apparaat zal opnieuw opstarten nadat de update is voltooid
- Gekopieerd naar klembord
-
- Welkom bij LSPosed
- U gebruikt de parasitaire manager, die een snelkoppeling kan maken of nog steeds kan openen vanuit een melding.
- Je gebruikt de parasiterende manager, die kan openen vanuit de melding.
- Snelkoppeling maken
- Nooit tonen
- Parasitaire manager aanbevolen
- LSPosed ondersteunt nu systeemparasitisatie om detectie te voorkomen, u kunt parasitaire manager openen vanuit een melding. Het wordt aanbevolen om de huidige applicatie te verwijderen.
-
- Opslaan
- Uitgebreide logs
- Modules Logbestanden
- Logboek wordt opgeslagen, even geduld a.u.b.
- Opgeslagen logboeken
- Opslaan mislukt:\n%s
- Wis het logboek nu
- Log succesvol gewist.
- Scroll naar boven
- Laden…
- Scroll naar beneden
- Herlaad
- Het logboek kan niet worden gewist
- Woord Omslag
- Uitgebreide logboek ingeschakeld
- Verbose logboek uitgeschakeld
-
- (geen beschrijving gegeven)
- Deze module vereist een nieuwere versie van Lsposed (%d) en kan dus niet worden geactiveerd
- Deze module is ontworpen voor een nieuwere Xposed-versie (%d) en daarom werken sommige functionaliteiten mogelijk niet
- Deze module heeft geen vereiste versie voor Xposed opgegeven.
- Deze module is gemaakt voor de Lsposed-versie %1$d, maar vanwege onverenigbare wijzigingen in de versie %2$d, is het uitgeschakeld
- Deze module kan niet worden geladen omdat deze op de SD-kaart is geïnstalleerd. Verplaats deze naar de interne opslag
- Verwijderen
- Module instellingen
- Bekijk in Repo
- Wilt u deze module verwijderen?
- Gedeïnstalleerd %1$s
- Verwijderen mislukt
- Module toevoegen aan gebruiker
- Toegevoegd %1$s naar gebruiker %2$s
- Het toevoegen van de module is mislukt
- Installeer naar gebruiker %s
- Wil je installeren %1$s naar gebruiker %2$s? Het wordt aanbevolen om handmatig te installeren, het forceren van installatie via LSPosed kan problemen veroorzaken.
- uitbreiden
- samenvouwen
-
- Opnieuw optimaliseren
- Optimaliseren…
- Optimalisatie voltooid.
- Lanceer het
- Optimalisatie mislukt of geretourneerde waarde is leeg.
- Optimalisatie is mislukt:
- Sorteer op applicatienaam
- Sorteer op pakketnaam
- Sorteer op installatietijd
- Sorteer op updatetijd
- Omkeren
- Systeem apps
- Sorteren
- Schakel module in
- U heeft geen app geselecteerd. Doorgaan?
- Games
- Modules
- Scopelijst opslaan is mislukt
- Versie: %1$s
- Aanbevolen
- U heeft geen app geselecteerd. Aanbevolen apps selecteren?
- Aanbevolen apps selecteren?
- LSPosed module is nog niet geactiveerd
- Aanbevolen
- Update beschikbaar: %1$s
- Module %1$s is uitgeschakeld omdat er geen app is geselecteerd.
- Systeem Framework
- Backup
- Backup
- Herstellen
- Forceer stop
- Gedwongen stoppen?
- Als je een app gedwongen stopt, kan deze onverwacht gedrag vertonen.
- Opnieuw opstarten is vereist om deze wijziging door te voeren
- Herstart
- Verbergen
-
- Bekijk in andere app
- App informatie
- ¯\\\\_(ツ)_\/¯\nNiets hier
-
- Framework
- Schakel uitgebreide logboeken uit
- Rapporteer problemen verzoek om uitgebreide logboeken op te nemen
- Zwarte donkere thema
- Gebruik het pure zwarte thema als het donkere thema is ingeschakeld
- Thema
- Backup en herstellen
- Backup of herstel de modulelijst en scooplijsten.
- Herstel de modulelijst en scooplijsten.
- Backup
- Opslaan mislukt:\n%s
- Schakel DocumentUI in
- Herstellen
- Herstellen mislukt:\n%s
- Netwerk
- DNS via HTTPS
- Tijdelijke oplossing voor DNS-vergiftiging in sommige landen
- Thema kleur
- Systeem thema kleur
- Forceer apps om launcher-pictogrammen weer te geven
- Na Android 10 mogen apps de launcher pictogrammen niet verbergen. Schakel de schakelaar uit om deze functie uit te schakelen.
- Systeem
- Taal
- Bijdragers van vertalingen
- Deelnemen aan vertaling
- Help ons %s te vertalen in uw taal
- Maak een snelkoppeling die parasitaire manager kan openen
- Snelkoppeling vastgezet
- De huidige standaard launcher ondersteunt geen pin-snelkoppelingen
- Statusmelding
- Toon een melding die parasitaire manager kan openen
- Kanaal bijwerken
- Stabiel
- Bèta
- Nachtelijke bouw
-
- Leesmij
- Publicaties
- Informatie
- Startpagina
- Broncode
- Medewerkers
- Middelen
- Open in de browser
- Laat oudere versies zien
- Geen release meer
- Kan moduleopslagplaats niet laden: %1$s
- Eerst te upgraden
- Geïnstalleerd
-
- - %d download
- - %d downloads
-
-
- Sakura
- Rood
- Roze
- Paars
- Donker paars
- Indigo
- Blauw
- Licht blauw
- Turquoise
- Groenblauw
- Groen
- Licht groen
- Limoen
- Geel
- Amber
- Oranje
- Diep oranje
- Bruin
- Blauw grijs
-
diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml
deleted file mode 100644
index 730e295ab..000000000
--- a/app/src/main/res/values-no/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Oversikt
- Moduler
-
- - %d modul aktivert
- - %d moduler aktivert
-
- Logger
- Innstillinger
- Tilbakemelding eller forslag
- Om
- Rapporter en feil
- Kodelager
- Alle moduler oppdatert
- Publisert på %s
- Oppdatert kl %s
-
- - %d modul kan oppgraderes
- - %d moduler kan oppgraderes
-
- Bli med i vår %2$s -kanal]]>
- null
- Installer
- Trykk for å installere LSPosed
- Ikke installert
- LSPC er ikke installert
- Aktivert
- Delvis aktivert
- SEPolicy er ikke lastet riktig
- Vennligst rapporter dette til Magisk utvikler.]]>
- Systemets ramme-injeksjon mislyktes
- Magisk eller noen lavkvalitets magnetiske moduler. Prøv å deaktivere andre Magiske moduler enn Riru og LSPosed eller sende full logg til utviklere.]]>
- Feil i systemprop
- Moduler kan godkjennes av og til.]]>
- Må oppdatere
- Vennligst installer den nyeste versjonen av LSPosed
- API versjon
- Rammeverk versjon
- Lederpakkenavn (Automatic Translation)
- System versjon
- Enhet
- System ABI
- Dex Optimizer Wrapper
- Aktivert
- Ikke aktivert
- Støttes
- Ustøttet
- Android-versjonen er ikke fornøyd
- Krasjet
- Montering mislyktes
- SELinux er tillatt
- SELinux policy er feil
- Oppdateringen LSPosed
- Bekreft oppdatering av LSPosed? Denne enheten vil starte på nytt etter fullført oppdatering
- Kopiert til utklippstavlen
-
- Velkommen til LSPosed
- Du bruker den parasittiske manageren, som kan opprette snarvei eller fortsatt åpne fra varsling.
- Du bruker parasittbehandleren, som kan åpnes fra varsling.
- Opprett snarvei
- Vis aldri
- Parasittisk leder anbefales
- LSPosed støtter nå systemparasittering for å unngå deteksjon, du kan åpne parasitic manager fra varsling. Det anbefales å avinstallere gjeldende applikasjon.
-
- Lagre
- Detaljert logg
- Modullogger
- Lagrer loggen, vennligst vent
- Logger lagret
- Kunne ikke lagre:\n%s
- Tøm logg nå
- Logg fjernet.
- Rull til toppen
- Laster…
- Rull til bunnen
- Reload
- Kunne ikke tømme loggen
- Ordfarget farget farget
- Detaljert logg aktivert
- Detaljert logg deaktivert
-
- (ingen beskrivelse er gitt)
- Denne modulen krever en nyere Xposed versjon (%d) og kan dermed ikke aktiveres
- Denne modulen er designet for en nyere Xposed-versjon (%d), og derfor kan det hende at enkelte funksjoner ikke fungerer
- Denne modulen spesifiserer ikke hvilken versjon av Xposed den trenger.
- Denne modulen var laget for Xposed versjon %1$d, men grunnet inkompatible endringer i versjon %2$der den blitt deaktivert
- Denne modulen kan ikke lastes fordi den er installert på SD-kortet, vennligst flytt den til intern lagring
- Avinstaller
- Modul innstillinger
- Vis i Repo
- Vil du avinstallere denne modulen?
- Avinstallert %1$s
- Avinstalleringen mislyktes
- Legg til modul i brukeren
- La til %1$s som bruker %2$s
- Legge til modul mislyktes
- Installer til bruker %s
- Ønsker du installere %1$s til brukeren %2$s? Det anbefales å installere manuelt, tvinge installasjonen via LSPosed kan forårsake problemer.
- utvid
- skjul
-
- Gjenoptimaliser
- Optimaliserer…
- Optimalisering fullført
- Start det
- Optimalisering mislyktes: returverdien er tom
- Optimalisering mislyktes:
- Navn på applikasjon
- Navn på pakke
- Installere tid
- Oppdaterings tid
- Revers
- System apper
- Sortering
- Aktiver modul
- Du valgte ingen app. Fortsette?
- Spill
- Moduler
- Kan ikke lagre områdeliste
- Versjon: %1$s
- Anbefalt
- Du valgte ingen applikasjoner. Velg anbefalte apper?
- Velg anbefalte apper?
- Xposed modul er ikke aktivert enda
- Anbefalt
- Oppdatering tilgjengelig: %1$s
- Modul %s er deaktivert siden ingen app valgt.
- Systemets rammeverk
- Sikkerhetskopi
- Sikkerhetskopi
- Gjenopprett
- Tving avslutning
- Tving stopp?
- Hvis du tvangsstopper en app, kan den oppføre seg uventet.
- Omstart kreves for at denne endringen skal gjelde
- Reboot
- Skjul
-
- Se i andre app
- Info om app
- ########\\\\_(∙)_\/ ″\nIngenting her
-
- Framework
- Deaktiver detaljerte logger
- Rapporter problemer forespørsel om å inkludere detaljerte logger
- Svart mørkt tema
- Bruk det svarte temaet hvis mørkt tema er aktivert
- Tema
- Sikkerhetskopiering og gjenoppretting
- Sikkerhetskopier modulliste og omfangslister.
- Gjenopprett modulliste og omfangslister.
- Sikkerhetskopi
- Klarte ikke å sikkerhetskopiere:\n%s
- Aktiver DocumentUI
- Gjenopprett
- Kunne ikke gjenopprette:\n%s
- Nettverk
- DNS over HTTPS
- Workaround DNS-forgiftning i noen nasjoner
- Tema farge
- System temafarge
- Tving apper til å vise programvelgerikoner
- Etter Android 10 er apper ikke tillatt å skjule programikonene sine. Slå av for å deaktivere denne systemfunksjonen.
- Systemadministrasjon
- Språk
- Oversettelse bidragsytere
- Delta i oversettelse
- Hjelp oss med å oversette %s til ditt språk
- Lag en snarvei som kan åpne parasitic manager
- Snarvei festet
- Den gjeldende standardstarteren støtter ikke pin-snarveier
- Statusvarsel
- Vis et varsel som kan åpne parasitic manager
- Oppdater kanal
- Stabil
- beta
- Nattlig bygg
-
- Lesmeg
- Utgivelser
- Informasjon
- Hjemmeside
- Kildekode
- Samarbeidspartnere
- Eiendeler
- Åpne i nettleser
- Vis eldre versjoner
- Ingen flere utgivelse
- Kunne ikke laste modulrepo: %s
- Oppgraderbar først
- Installert
-
- - %d nedlasting
- - %d nedlastinger
-
-
- Sakura
- Rød
- Rosa
- Lilla
- Dyp lilla
- Indigo
- Blå
- Lys blå
- Turkis
- Blågrønn
- Grønn
- Lys grønn
- Limegrønn
- Gul
- Rav
- Oransje
- Dyp oransje
- Brun
- Blå grå
-
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
deleted file mode 100644
index c5d514c62..000000000
--- a/app/src/main/res/values-pl/strings.xml
+++ /dev/null
@@ -1,248 +0,0 @@
-
-
-
-
- Przegląd
- Moduły
-
- - %d moduł aktywowany
- - Aktywowano %d modułu/ów
- - Włączono %d moduły/ów
- - Włączono %d moduły/ów
-
- Logi
- Ustawienia
- Opinie lub sugestie
- O aplikacji
- Zgłoś problem
- Repozytorium
- Wszystkie moduły są aktualne
- Opublikowano na stronie %s
- Zaktualizowany na stronie %s
-
- - %d moduł możliwy do aktualizacji
- - %d moduły z możliwością aktualizacji
- - %d moduły z możliwością aktualizacji
- - %d modułów z możliwością aktualizacji
-
- Dołącz do naszego kanału %2$s]]>
- Adiker, xAzvest
- Instaluj
- Dotknij, aby zainstalować LSPosed
- Nie zainstalowano
- LSPosed nie jest zainstalowany
- Aktywowano
- Częściowo aktywowano
- SEPolicy nie jest prawidłowo załadowane
- Proszę to zgłosić do twórcy Magiska .]]>
- Wstrzyknięcie Frameworku Systemowego nie powiodło się
- Magiska lub niektóre moduły Magisk niskiej jakości. Spróbuj wyłączyć moduły Magisk inne niż Riru i LSPosed lub przesłać pełny log programistom.]]>
- Nieprawidłowy system prop
- Moduły mogą czasami działać niepoprawnie.]]>
- Konieczna aktualizacja
- Proszę zainstalować najnowszą wersję LSPosed
- Porady dla programisty modułów
- Wyłącz optymalizacje wdrażania w Android Studio lub użyj polecenia `gradlew installDebug`, aby zainstalować. W przeciwnym razie moduł APK nie zostanie zaktualizowany.
- Wersja API
- Wersja frameworka
- Nazwa pakietu menedżera
- Wersja systemu
- Urządzenie
- ABI systemu
- Optymalizator Dex Optimizer Wrapper
- Włączone
- Wyłączone
- Obsługiwana strona
- Nieobsługiwane
- Niezadowolenie z wersji na Androida
- Rozbita
- Montowanie nie powiodło się
- SELinux jest permisywny
- Zasady SELinuxa są nieprawidłowe
- Zaktualizuj LSPosed
- Czy chcesz zatwierdzić aktualizację LSPosed? Urządzenie zostanie uruchomione ponownie po zakończeniu aktualizacji
- Skopiowane do schowka
-
- Witamy w LSPosed
- Używasz menedżera pasożytów, który może utworzyć skrót na ekranie głównym lub nadal być otwieranym z powiadomienia.
- Używasz menedżera pasożytów, który może być otwierany z powiadomienia.
- Utwórz skrót
- Nigdy nie pokazuj
- Zalecany Menedżer Pasożytowy
- LSPosed teraz obsługuje pasożytowanie systemowe by uniknąć wykrycia, możesz otworzyć menedżera pasożytów z okna powiadomień. Zalecane jest odinstalowanie obecnej aplikacji.
-
- Zapisz
- Logi verbose
- Logi modułów
- Zapisywanie dziennika, poczekaj
- Zapisane logi
- Nie udało się zapisać:\n%s
- Wyczyść log teraz
- Logi zostały pomyślnie wyczyszczone.
- Przewiń na górę
- Ładowanie…
- Przewiń na dół
- Przeładuj
- Nie udało się wyczyścić logu
- Zawijanie wierszy
- Logi verbose włączone
- Logi verbose wyłączone
-
- (nie podano opisu)
- Ten moduł wymaga nowszej wersji Xposed (%d) przez co nie może być aktywowany
- Ten moduł został zaprojektowany dla nowej wersji Xposed (%d) i dlatego niektóre funkcje mogą nie działać
- Ten moduł nie określa wymaganej wersji Xposed.
- Ten moduł został utworzony dla Xposed w wersji %1$d, ale ze względu na niekompatybilne zmiany w wersji %2$d, został wyłączony
- Ten moduł nie może być załadowany, ponieważ jest zainstalowany na karcie SD, przenieś go do pamięci wewnętrznej
- Odinstaluj
- Ustawienia modułu
- Wyświetl w repozytorium
- Czy chcesz odinstalować ten moduł?
- Odinstalowano %1$s
- Odinstalowanie nie powiodło się
- Dodaj moduł do użytkownika
- Dodano %1$s do użytkownika %2$s
- Dodawanie modułu nie powiodło się
- Zainstaluj dla użytkownika %s
- Chcesz zainstalować %1$s dla użytkownika %2$s? Zalecana jest ręczna instalacja, wymuszanie instalacji przez LSPosed może powodować problemy.
- rozwiń
- zwiń
-
- Ponownie zoptymalizuj
- Optymalizowanie…
- Optymalizacja zakończona
- Uruchom
- Optymalizacja nie powiodła się: zwrócona wartość jest pusta
- Optymalizacja nie powiodła się:
- Nazwa aplikacji
- Nazwa pakietu
- Czas instalacji
- Czas aktualizacji
- Odwróć
- Aplikacje systemowe
- Sortowanie
- Włącz moduł
- Nie wybrałeś żadnej aplikacji. Kontynuować?
- Gry
- Moduły
- Nie udało się zapisać listy zakresu
- Wersja: %1$s
- Zaznacz
- Zalecane
- Nie wybrałeś żadnej aplikacji. Wybrać rekomendowane aplikacje?
- Wybrać zalecane aplikacje?
- Wszystkie
- Żadne
- Automatycznie
- Moduł Xposed nie jest jeszcze aktywowany
- Zalecane
- Dostępna aktualizacja: %1$s
- Moduł %s został wyłączony, ponieważ nie wybrano żadnej aplikacji.
- System Framework
- Kopia zapasowa
- Kopia zapasowa
- Przywróć
- Wymuś zatrzymanie
- Wymusić zatrzymanie?
- Jeśli wymusisz zatrzymanie aplikacji, może ona zadziałać nieprawidłowo.
- Restart jest wymagany aby ta zmiana została zastosowana
- Restart
- Ukryj
-
- Wyświetl w innej aplikacji
- Informacje o aplikacji
- ¯\\\\_(ツ)_\/¯\nNic tu nie ma
-
- Framework
- Wyłącz logi
- Zgłaszanie problemów wymaga dołączenia pełnych dzienników
- Czarny ciemny motyw
- Użyj czarnego motywu, jeśli ciemny motyw jest włączony
- Motyw
- Kopia zapasowa i przywracanie
- Kopia zapasowa listy modułów i zakresów.
- Przywrócenie listy modułów i listy zakresów.
- Kopia zapasowa
- Nie udało się utworzyć kopii zapasowej:\n%s
- Proszę włączyć DocumentUI
- Przywróć
- Nie udało się przywrócić:\n%s
- Sieć
- DNS przez HTTPS
- Obejście zatruwania DNS w niektórych krajach
- Kolor motywu
- Kolor akcentu systemowego
- Wymuś aplikacjom pokazywanie ikon launchera
- Po Android 10 aplikacje nie mogą ukrywać swoich ikon launchera. Wyłącz przełącznik, aby zablokować tę funkcję systemową.
- System
- Język
- Tłumacze
- Weź udział w tłumaczeniu
- Pomóż nam przetłumaczyć %s na Twój język
- Utwórz skrót, za pomocą którego otworzysz menadżera \"pasożytów\"
- Skrót przypięty
- Bieżący domyślny launcher nie wspiera skrótów pinezkowych
- Status powiadomień
- Pokazuj powiadomienie, za pomocą którego otworzysz menadżera pasożytów
- Kanał aktualizacji
- Stabilny
- Beta
- Wersja nocna
-
- Readme
- Wersje
- Informacje
- Strona główna
- Kod źródłowy
- Współtwórcy
- Assety
- Otwórz w przeglądarce
- Pokaż starsze wersje
- Nie ma więcej wersji
- Nie udało się załadować repozytorium modułu: %s
- Możliwość uaktualnienia w pierwszej kolejności
- Zainstalowane
-
- - %d pobierz
- - %d pobieranie
- - %d pobieranie
- - %d pobrania
-
-
- Sakura
- Czerwony
- Różowy
- Fioletowy
- Ciemny fioletowy
- Indigo
- Niebieski
- Jasnoniebieski
- Cyjanowy
- Turkusowy
- Zielony
- Jasnozielony
- Limonkowy
- Żółty
- Bursztynowy
- Pomarańczowy
- Ciemny pomarańczowy
- Brązowy
- Niebiesko szary
-
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
deleted file mode 100644
index 08c4a0dee..000000000
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- Início
- Módulos
-
- - %d módulo ativo
- - %d módulos ativos
-
- Registros
- Configurações
- Feedback ou sugestão
- Sobre
- Relatar problema
- Repositório
- Todos os módulos estão atualizados
- Publicado em %s
- Atualizado em %s
-
- - %d módulo para atualizar
- - %d módulos para atualizar
-
- Participe do nosso canal do %2$s]]>
- VD171, Dan, Hitalo, Luiz Renato, PedriR19 e Igor
- Instalar
- Toque para instalar o LSPosed
- Não instalado
- O LSPosed não está instalado
- Ativado
- Parcialmente ativado
- SEPolicy não está devidamente carregado
- Por favor, relate isso ao desenvolvedor do Magisk .]]>
- Falha na injeção do Framework do Sistema
- Magisk ou por algum módulo do Magisk de baixa qualidade. Por favor, tente desativar os módulos do Magisk, exceto o LSPosed (e o Riru, caso esteja em uso). Ou envie todos os registros para os desenvolvedores.]]>
- Propriedade do sistema incorreta
- Alguns módulos podem falhar ocasionalmente.]]>
- Necessário atualizar
- Por favor, instale a versão mais recente do LSPosed.
- Dicas para desenvolvedores de módulos
- Por favor, desative a implantação de otimizações no Android Studio ou use o comando `gradlew installDebug` para instalar. Caso contrário, o módulo APK não será atualizado.
- Versão da API
- Versão do framework
- Nome do pacote do gerenciador
- Versão do sistema
- Dispositivo
- ABI do sistema
- Invólucro do otimizador dex
- Ativado
- Desativado
- Compatível
- Incompatível
- Versão do Android insuficiente
- Falhou
- Falha na montagem
- O SELinux está permissivo
- A política do SELinux está incorreta
- Atualizar o LSPosed
- Confirmar atualização do LSPosed? O dispositivo irá reiniciar após completar a atualização.
- Copiado para a área de transferência
-
- Bem-vindo ao LSPosed
- Você está usando o gerenciador parasita, você pode criar o atalho ou então abrir a partir da notificação.
- Você está usando o gerenciador parasita, ele pode ser aberto a partir da notificação.
- Criar atalho do gerenciador parasita
- Não mostrar novamente
- Gerenciador parasita recomendado
- O LSPosed agora oferece suporte a parasitização do sistema para evitar detecções, você pode abrir o gerenciador parasita a partir da notificação. É recomendado desinstalar o app atual.
-
- Salvar
- Registros detalhados
- Registros dos módulos
- Salvando registro, aguarde.
- Registros salvos
- Falha ao salvar:\n%s
- Limpar registros
- Registros limpo com sucesso.
- Rolar para cima
- Carregando…
- Rolar para baixo
- Recarregar
- Falha ao limpar os registros
- Quebra de linha
- Registros detalhados ativados
- Registros detalhados desativados
-
- (nenhuma descrição fornecida)
- Este módulo exige uma versão mais recente do LSPosed (%d). Por isso, não poderá ser ativado.
- Este módulo foi projetado para uma versão mais recente do LSPosed (%d). Por isso, algumas funcionalidades podem não funcionar.
- Este módulo não especifica qual a versão necessária do LSPosed.
- Este módulo foi criado para a versão %1$d do LSPosed, mas devido a alterações incompatíveis na versão %2$d, ele foi desativado.
- Este módulo não pode ser carregado porque está instalado no cartão de memória. Por favor, mova para o armazenamento interno.
- Desinstalar
- Abrir módulo
- Ver no repositório
- Deseja desinstalar este módulo?
- %1$s desinstalado
- Falha ao desinstalar
- Adicionar módulo ao usuário
- %1$s foi adicionado ao usuário %2$s
- Falha ao adicionar módulo
- Instalar no usuário %s
- Deseja instalar %1$s no usuário %2$s? É recomendado instalar manualmente. Forçar a instalação via LSPosed pode causar problemas.
- expandir
- recolher
-
- Re-otimizar
- Otimizando…
- Otimização completa
- Abrir app
- A otimização falhou: não houve nenhum retorno
- Falha na otimização:
- Nome do app
- Nome do pacote
- Data de instalação
- Data de atualização
- Inverter
- Apps do sistema
- Ordenar
- Ativar módulo
- Você não selecionou nenhum app. Deseja continuar?
- Jogos
- Módulos
- Falha ao salvar lista de escopos
- Versão: %1$s
- Selecionar
- Recomendações
- Você não selecionou nenhum app. Deseja selecionar os apps recomendados?
- Selecionar os apps recomendados?
- Todos
- Nenhum
- Auto-incluir
- O módulo ainda não está ativo
- Recomendações
- Atualização disponível: %1$s
- O módulo %s foi desativado porque nenhum app foi selecionado.
- Framework do Sistema
- Fazer backup
- Fazer backup
- Restaurar backup
- Forçar parada
- Forçar parada?
- Se você forçar a parada de um app, problemas podem ocorrer.
- Necessário reiniciar para aplicar a alteração
- Reiniciar
- Ocultar
-
- Ver em outro app
- Informações do app
- ¯\\_(ツ)_\/¯\nNada aqui
-
- Framework
- Desativar registros detalhados
- Relate o problema para incluir os registros detalhados
- Tema preto puro
- Use o tema preto puro se o tema escuro estiver ativo
- Tema
- Backup e restauração
- Faça um backup da lista de módulos e escopos
- Restaure a lista de módulos e escopos
- Fazer backup
- Falha ao fazer backup:\n%s
- Por favor, ative o DocumentUI.
- Restaurar backup
- Falha ao restaurar:\n%s
- Rede
- DNS sobre HTTPS
- Solução alternativa para envenenamento de DNS em alguns países
- Cor do tema
- Cor de destaque do sistema
- Forçar exibição de ícones dos apps
- Após o Android 10, alguns apps não estão autorizados a ocultarem seus ícones. Desative esta opção para desligar este recurso.
- Sistema
- Idioma
- Tradutores
- Participe das traduções
- Ajude-nos a traduzir o %s para o Português (Brasil)
- Crie um atalho para abrir o gerenciador parasita
- Atalho fixado
- A launcher padrão atual não suporta atalhos fixados
- Notificação de estado
- Mostre uma notificação que possa abrir o gerenciador parasita
- Canal de atualizações
- Estável
- Beta
- Nightly
-
- Leia-me
- Versões
- Informações
- Site
- Código-fonte
- Colaboradores
- Conteúdos
- Abrir no navegador
- Mostrar versões anteriores
- Não há mais versões
- Falha ao carregar o repositório do módulo %s
- Atualizável primeiro
- Instalado
-
- - %d baixado
- - %d baixados
-
-
- Sakura
- Vermelho
- Rosa
- Roxo
- Roxo profundo
- Azul-anil
- Azul
- Azul claro
- Ciano
- Verde-azulado
- Verde
- Verde claro
- Verde-limão
- Amarelo
- Âmbar
- Laranja
- Laranja profundo
- Marrom
- Cinza-azulado
-
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
deleted file mode 100644
index f511f80c0..000000000
--- a/app/src/main/res/values-pt/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Resumo
- Módulos
-
- - %d módulo ativo
- - %d módulos ativos
-
- Registros
- Configurações
- Comentários e Sugestões
- Sobre
- Relatar problema
- Repositório
- Todos os módulos estão atualizados
- Publicado em %s
- Atualizado em %s
-
- - %d módulo para atualizar
- - %d módulos para atualizar
-
- Entre em nosso canal no %2$s]]>
- VD171, Dan, Hitalo, Luiz Renato, PedriR19
- Instalar
- Toque para instalar o LSPosed
- Não instalado
- O LSPosed não está instalado
- Ativado
- Parcialmente Ativado
- SEPolicy não está devidamente carregado
- Por favor informe Magisk desenvolvedor.]]>
- Falha na injeção ao Framework do Sistema
- Magisk ou por algum módulo do Magisk de baixa qualidade. Por favor, tente desativar os módulos do Magisk, exceto o LSPosed (e o Riru, caso esteja em uso). Ou envie todos os registros para os desenvolvedores.]]>
- Propriedade do sistema incorreta
- Alguns módulos podem falhar ocasionalmente.]]>
- Necessário atualizar
- Por favor, instale a versão mais recente do LSPosed
- Versão da API
- Versão do Framework
- Nome do pacote do gestor
- Versão do Sistema
- Aparelho
- Interfaces Binárias do Sistema
- Otimização Encapsulamento Dex
- Ativado
- Desativado
- Compatível
- Incompatível
- Versão do Android insuficiente
- Falhou
- Falha na montagem
- O SELinux está permissivo
- A política do SELinux está incorreta
- Atualizar o LSPosed
- Confirmar atualização do LSPosed? O aparelho irá reiniciar após completar a atualização
- Copiado para a área de transferência
-
- Bem-vindo ao LSPosed
- Você está usando o gerenciador parasita, você pode criar o atalho ou então abrir a partir da notificação.
- Você está usando o gerenciador parasita, ele pode ser aberto a partir da notificação.
- Criar Atalho do Gerenciador Parasita
- Não mostrar novamente
- É recomendado o gerenciador parasita
- O LSPosed agora oferece suporte a parasitização do sistema para evitar detecções, você pode abrir o gerenciador parasita a partir da notificação. É recomendado desinstalar o aplicativo atual.
-
- Salvar
- Registros Detalhados
- Registros dos Módulos
- A guardar o registo, aguarde
- Registros salvos
- Falha ao salvar:\n%s
- Limpar Registros
- Registro limpo com sucesso.
- Rolar para o topo
- Carregando…
- Rolar para baixo
- Recarregar
- Falha ao limpar os registros
- Quebra de Linha
- Registros detalhados ativados
- Registros detalhados desativados
-
- (nenhuma descrição fornecida)
- Este módulo exige uma versão mais recente do Xposed (%d). Por isso, não poderá ser ativado
- Este módulo foi concebido para uma versão mais recente do Xposed (%d) e, portanto, algumas funcionalidades podem não funcionar
- Este módulo não especifica qual a versão necessária do Xposed.
- Este módulo foi criado para a versão %1$d do Xposed. Mas, devido mudanças incompatíveis na versão %2$d, ele foi desativado
- Este módulo não pode ser carregado porque está instalado no cartão de memória. Por favor, mova para o armazenamento interno
- Desinstalar
- Abrir módulo
- Ver no Repositório
- Você deseja desinstalar este módulo?
- %1$s desinstalado
- Falha ao desinstalar
- Adicionar módulo no usuário
- %1$s foi adicionado no usuário %2$s
- Falha ao adicionar módulo
- Instalar no usuário %s
- Deseja instalar %1$s no usuário %2$s? É recomendado instalar manualmente. Forçar a instalação via LSPosed pode causar problemas.
- expandir
- recolher
-
- Re-otimizar
- Otimizando…
- Otimização completa
- Abrir app
- A otimização falhou: não houve nenhum retorno
- Falha na otimização:
- Nome do Módulo
- Nome do Pacote
- Data de Instalação
- Data de Atualização
- Inverter
- Apps do Sistema
- Ordenar
- Ativar módulo
- Você não selecionou nenhum aplicativo. Deseja continuar?
- Jogos
- Módulos
- Falha ao salvar lista de escopos
- Versão: %1$s
- Recomendações
- Você não selecionou nenhum aplicativo. Deseja selecionar os aplicativos recomendados?
- Selecionar os aplicativos recomendados?
- O módulo ainda não está ativo
- Recomendações
- Atualização disponível: %1$s
- O módulo %s foi desativado porque nenhum aplicativo foi selecionado.
- Framework do Sistema
- Fazer Backup
- Fazer Backup
- Restaurar Backup
- Forçar parada
- Forçar parada?
- Se forçar a parada de um aplicativo, ele pode apresentar falhas.
- Necessário reiniciar para aplicar alteração
- Reiniciar
- Esconder
-
- Ver na Play Store
- Informações do app
- ¯\\\\_(ツ)_\/¯\nNada encontrado
-
- Framework
- Desativar registros detalhados
- Relate o problema para incluir os registros detalhados
- Usar Tema Preto Puro
- Use o tema preto puro se o tema noturno estiver ativo
- Tema
- Backup e Restauração
- Faz uma cópia de segurança da lista de módulos e escopos
- Faz a restauração de uma cópia de segurança da lista de módulos e escopos
- Fazer Backup
- Falha ao fazer backup:\n%s
- Por favor, ative DocumentUI
- Restaurar Backup
- Falha ao restaurar:\n%s
- Rede
- Ativar DNS sobre HTTPS
- Solução alternativa para envenenamento de DNS em alguns países
- Cor do Tema
- Cor de destaque do sistema
- Forçar exibição de ícones dos apps
- Após o Android 10, alguns apps não estão autorizados a ocultarem seus ícones. Desative esta opção para desligar este recurso
- Sistema
- Idioma
- Tradutores
- Participe das Traduções
- Ajude a traduzir o %s para o Português 🇵🇹🇧🇷
- Criar um atalho para abrir gerenciador parasita
- Atalho com alfinete
- O lançador padrão actual não suporta atalhos de pinos
- Notificação de estado
- Mostrar uma notificação que possa abrir o gerenciador parasita
- Canal de Atualizações
- Estável
- Beta
- Nightly
-
- Leia-me
- Versões
- Informações
- Website
- Código-Fonte
- Colaboradores
- Conteúdos
- Abrir no navegador
- Mostrar versões anteriores
- Não há mais versões
- Falha ao carregar o repositório de módulos: %s
- Atualizável Primeiro
- Instalado
-
- - %d baixado
- - %d baixados
-
-
- Sakura
- Vermelho
- Rosa
- Roxo
- Roxo Escuro
- Azul-anil
- Azul
- Azul Claro
- Ciano
- Verde-azulado
- Verde
- Verde Claro
- Verde-limão
- Amarelo
- Âmbar
- Laranja
- Laranja Escuro
- Marrom
- Azul-acinzentado
-
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
deleted file mode 100644
index 08dcbfe04..000000000
--- a/app/src/main/res/values-ro/strings.xml
+++ /dev/null
@@ -1,245 +0,0 @@
-
-
-
-
- Sumar
- Module
-
- - %d modul activat
- - %d module activate
- - %d de module activate
-
- Jurnale
- Setări
- Feedback sau sugestii
- Despre
- Semnalați o problemă
- Repozitoriu
- Modulele dv. sunt la zi
- Publicat în %s
- Actualizat în %s
-
- - %d modul de actualizat
- - %d module de actualizat
- - %d de module de actualizat
-
- Alăturați-vă grupului nostru %2$s]]>
- Chele Ion, Igor Sorocean, br l12, Munteanu Roveliu, NicoaraAlex, Sergiu Alexa
- Instalare
- Atingeți pentru a instala LSPosed
- Nu este instalat
- LSPosed nu este instalat
- Activat
- Activat parțial
- Politica SE nu este încărcată corect
- Vă rugăm să vă adresați dezvoltatorului Magisk .]]>
- Injectarea în Sistemul Android a eșuat
- Magisk sau unele module de slabă calitate. Vă rugăm încercați să dezactivați modulele Magisk, exceptând Riru și LSPosed, sau să trimiteți dezvoltatorilor jurnalul complet.]]>
- Proprietăți de sistem invalide
- Este posibil ca modulele să funcționeze intermitent.]]>
- Actualizare necesară
- Vă rugăm să actualizați LSPosed
- Sfaturi pentru dezvoltatorii de module
- Vă rugăm să dezactivați optimizările automate oferite de Android Studio sau să executați `gradlew installDebug` pentru instalare. În caz contrar, aplicația nu va putea fi actualizată.
- Versiune API
- Versiune platformă
- Numele pachetului manager
- Versiune sistem
- Dispozitiv
- Model de interfață binară
- Interfață pentru optimizarea compilării
- Activat(ă)
- Dezactivat(ă)
- Acceptat(ă)
- Neacceptat(ă)
- Versiune Android neacceptată
- Oprit forțat
- Montarea a eșuat
- SELinux este facultativ
- Politica SELinux este incorectă
- Actualizați LSPosed
- Confirmați actualizarea LSPosed? Dispozitivul va reporni după finalizarea actualizării.
- Copiat în clipboard
-
- Bine ați venit la LSPosed!
- Folosiți managerul parazitar, care poate fi deschis dintr-o scurtătură creată de acesta sau din centrul de notificări.
- Folosiți managerul parazitar, care poate fi deschis din centrul de notificări.
- Creează scurtătură
- Nu afișați din nou
- Managerul parazitar este recomandat
- LSPosed oferă posibilitatea parazitării sistemului, pentru a evita detectarea. Puteți deschide managerul parazitar din centrul de notificări. Se recomandă dezinstalarea aplicației curente.
-
- Salvare
- Jurnal detaliat
- Jurnal module
- Se salvează jurnalul, vă rugăm să așteptați
- Jurnal salvat
- Salvarea a eșuat:\n%s
- Golire jurnal
- Jurnalul a fost golit.
- Derulare la început
- Se încarcă…
- Derulare la final
- Reîncărcare
- Golirea jurnalului a eșuat
- Încadrare cuvinte
- Jurnal detaliat activat
- Jurnal detaliat dezactivat
-
- (fără descriere)
- Acest modul nu poate fi activat, deoarece necesită o versiune Xposed mai nouă (%d).
- Acest modul este proiectat pentru o versiune mai nouă a Xposed (%d). Este posibil ca unele funcții să nu poată fi activate.
- Acest modul nu specifică versiunea Xposed pentru care a fost proiectat.
- Acest modul a fost proiectat pentru versiunea Xposed %1$d, dar a fost dezactivat din cauza modificărilor incompatibile din versiunea %2$d.
- Acest modul nu poate fi încărcat, deoarece este instalat pe cardul SD. Vă rugăm să îl mutați pe stocarea internă.
- Dezinstalare
- Setări modul
- Vizualizați în repozitoriu
- Doriți să dezinstalați acest modul?
- %1$s a fost dezinstalat
- Dezinstalare nereușită
- Adăugare modul pentru utilizator
- %1$s adăugat utilizatorului %2$s
- Adăugarea modulului a eșuat
- Instalare modul pentru utilizatorul %s
- Doriți să instalați %1$s pentru utilizatorul %2$s? Este recomandat să instalați manual, forțarea instalării prin LSPosed poate cauza probleme.
- extindere
- restrângere
-
- Re-compilare
- Se compilează…
- Compilare finalizată
- Lansare
- Compilare eșuată: valoarea returnată este goală
- Compilare eșuată:
- Numele aplicației
- Numele pachetului
- Data instalării
- Data actualizării
- Ordine inversă
- Aplicații de sistem
- Sortare după
- Activare modul
- Nu ați selectat nicio aplicație. Continuați?
- Jocuri
- Module
- Actualizarea configurației a eșuat
- Versiune: %1$s
- Selectați
- Recomandate
- Nu ați selectat nicio aplicație. Selectați aplicațiile recomandate?
- Selectați aplicațiile recomandate?
- Toate
- Nimic
- Automat, la instalare
- Modulul Xposed nu este încă activat
- Recomandate
- Actualizare disponibilă: %1$s
- Nu ați selectat nicio aplicație. Modulul %s a fost dezactivat.
- Sistemul Android
- Copie de rezervă
- Realizare copie
- Restaurare
- Opriți forțat
- Opriți forțat?
- Dacă forțați oprirea unei aplicații, aceasta se poate comporta necorespunzător.
- Repornirea este necesară pentru ca modificarea să aibă efect
- Repornire
- Ascundeți
-
- Vizualizați în altă aplicație
- Informații aplicație
- ¯\\_(ツ)_\/¯\nAici nu e nimic
-
- Comportament
- Dezactivare jurnale detaliate
- Rapoartele de depanare necesită includerea de jurnale detaliate
- Temă întunecată
- Utilizați negru pur pentru tema întunecată
- Temă
- Copie de rezervă și restaurare
- Exportă lista modulelor și configurația lor.
- Restaurează lista modulelor și configurația lor.
- Realizați o copie de rezervă
- Copia de rezervă a eșuat:\n%s
- Vă rugăm activați aplicația „Fișiere“
- Restaurați o copie de rezervă
- Restaurarea a eșuat:\n%s
- Rețea
- DNS prin HTTPS
- Încearcă evitarea limitărilor DNS din unele țări
- Culoarea temei
- Culoarea temei de sistem
- Afișare forțată a aplicațiilor în lansator
- După Android 10, aplicațiile sunt forțate să afișeze pictograme în lansator. Setarea dezactivează această funcție de sistem.
- Sistem
- Limbă
- Traducători
- Contribuiți la traducere
- Ajutați-ne să traducem %s în limba dv.
- Creează o scurtătură pentru managerul parazitar
- Scurtătură creată
- Lansatorul implicit nu acceptă scurtături
- Notificare de stare
- Afișează o notificare pentru deschiderea managerului parazitar
- Canal de actualizare
- Stabil
- Beta
- Nightly
-
- A se citi mai întâi
- Versiuni
- Informații
- Pagină principală
- Cod sursă
- Colaboratori
- Fișiere
- Deschide în browser
- Versiuni mai vechi
- Nu există alte versiuni
- Încărcarea repozitoriului a eșuat: %s
- Actualizările întâi
- Instalat
-
- - %d descărcare
- - %d descărcări
- - %d de descărcări
-
-
- Roz-cireș
- Roșu
- Roz
- Violet
- Violet-închis
- Indigo
- Albastru
- Albastru-deschis
- Albastru-verzui
- Verde-albăstrui
- Verde
- Verde-deschis
- Verde-lămâie
- Galben
- Chihlimbar
- Portocaliu
- Portocaliu-închis
- Maro
- Gri-albăstrui
-
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
deleted file mode 100644
index e73c850f8..000000000
--- a/app/src/main/res/values-ru/strings.xml
+++ /dev/null
@@ -1,248 +0,0 @@
-
-
-
-
- Обзор
- Модули
-
- - Включен %d модуль
- - Включены %d модуля
- - Включено %d модулей
- - Включено %d модулей
-
- Журналы
- Настройки
- Обратная связь
- О приложении
- Сообщить о проблеме
- Репозиторий
- Все модули в актуальном состоянии
- Опубликовано %s
- Обновлено %s
-
- - Доступно обновление %d модуля
- - Доступны обновления %d модулей
- - Доступны обновления %d модулей
- - Доступны обновления %d модулей
-
- Заходите на наш %2$s-канал]]>
- Alеxеy, ded Arkash
- Установить
- Нажмите, чтобы установить LSPosed
- Не установлен
- LSPosed не установлен!
- Активирован
- Частично активирован
- SEPolicy загружен неправильно
- Пожалуйста, сообщите об этом разработчикам Magisk .]]>
- Системный фреймворк (System Framework): ошибка внедрения
- Magisk или его несовместимыми модулями. Попробуйте отключить модули Magisk, кроме Riru и LSPosed, или пришлите полный журнал разработчикам.]]>
- Неправильные системные настройки
- Модули могут не работать должным образом.]]>
- Требуется обновление
- Установите последнюю версию LSPosed!
- Советы разработчику модулей
- Пожалуйста, отключите оптимизацию развёртывания в Android Studio или используйте команду `gradlew installDebug` для установки. В противном случае .apk-модуль не обновится.
- Версия API
- Версия framework
- Имя пакета менеджера
- Версия системы
- Устройство
- Разрядность системы (ABI)
- Оптимизатор Dex
- Вкл.
- Выкл.
- Поддерживается
- Не поддерживается
- Версия Android не подходит
- Сбой
- Ошибка монтирования
- SELinux в состоянии Permissive
- Неправильная политика SELinux
- Обновление LSPosed
- Подтвердить обновление LSPosed? Устройство перезагрузится после завершения обновления
- Скопировано в буфер обмена
-
- Добро пожаловать в LSPosed
- Вы используете parasitic-менеджер (т. е. скрытый режим): можно создать его ярлык на рабочем столе либо по-прежнему открывать через уведомление.
- Вы используете parasitic-менеджер (т. е. скрытый режим), который можно открывать через уведомление.
- Создать ярлык
- Больше не показывать
- Рекомендуется parasitic-менеджер
- LSPosed теперь поддерживает «паразитирование», чтобы избежать обнаружение системой, — т. н. скрытый режим. Вы сможете открывать parasitic-менеджер LSPosed через уведомление. Текущее приложение рекомендуется удалить.
-
- Сохранить
- Подробный журнал
- Журнал модулей
- Журнал сохраняется, подождите
- Журнал сохранён
- Ошибка сохранения:\n%s
- Очистка журнала
- Журнал успешно очищен.
- Промотать вверх
- Загрузка…
- Промотать вниз
- Загрузить снова
- Ошибка очистки журнала
- Перенос текста
- Подробный журнал включен
- Подробный журнал выключен
-
- (описание отсутствует)
- Данному модулю требуется более новая версия Xposed/LSPosed (%d), поэтому активация невозможна
- Данный модуль разработан для более новой версии Xposed/LSPosed (%d), поэтому некоторые функции могут не работать
- В данном модуле не указана версия Xposed/LSPosed, необходимая для его работы.
- Данный модуль был создан для Xposed/LSPosed версии %1$d, но ввиду несовместимых изменений в версии %2$d он был выключен
- Данный модуль не может быть загружен, так как установлен на SD-карту. Переместите его во внутреннюю память
- Удалить
- Настройки модуля
- Смотреть в репо
- Вы точно хотите удалить данный модуль?
- %1$s удалён
- Удаление не удалось
- Добавление модуля пользователю
- %1$s добавлен (пользователь %2$s)
- Добавление модуля не удалось
- Установка (пользователь %s)
- Вы точно хотите установить %1$s (пользователь %2$s)? Рекомендуется устанавливать вручную, принудительная установка через LSPosed может вызвать проблемы.
- развернуть
- свернуть
-
- Повторная оптимизация
- Оптимизация…
- Оптимизация завершена!
- Запустить
- Ошибка оптимизации: возвращено пустое значение!
- Ошибка оптимизации:
- По названию приложения
- По названию пакета
- По времени установки
- По времени обновления
- Обратная сортировка
- Системные приложения
- Сортировка
- Включить модуль
- Вы не выбрали ни одного приложения. Продолжить?
- Игры
- Модули
- Ошибка сохранения списка «охвата»
- Версия: %1$s
- Выберите
- Рекомендуемые
- Вы не выбрали ни одного приложения. Выбрать рекомендуемые приложения?
- Выбрать рекомендуемые приложения?
- Все
- Нет
- Автовключение
- Модуль Xposed пока не активирован
- Рекомендуемые
- Доступно обновление: %1$s
- Модуль %s выключен, так как не выбрано ни одно приложение.
- Системный фреймворк
- Резервная копия
- Создать
- Восстановить
- Остановить
- Остановить приложение?
- Принудительная остановка приложения может отразиться на его функциональности.
- Для применения требуется перезагрузка
- Перезагрузить
- Скрыть
-
- Посмотреть в другом приложении
- О приложении
- ¯\\_(ツ)_\/¯\nЗдесь ничего нет
-
- Framework
- Выключить подробный журнал
- Отчёт об ошибке требует включение подробного журнала
- Чёрная тема
- Использовать чёрную тему, когда включена тёмная тема
- Тема
- Резервное копирование и восстановление
- Резервная копия списка модулей и списков «охвата».
- Восстановление списка модулей и списков «охвата».
- Резервная копия
- Ошибка создания резервной копии:\n%s
- Пожалуйста, включите DocumentUI
- Восстановить
- Ошибка восстановления:\n%s
- Сеть
- DNS через HTTPS
- Шифрование DNS-трафика для обхода ряда ограничений в сети
- Акцент темы
- Системный цвет акцента
- Принудительно показывать иконку модуля
- После Android 10 приложениям запрещено скрывать иконку их лаунчера. Выключите переключатель, чтобы отключить эту системную функцию.
- Система
- Язык приложения
- Авторы перевода
- Принять участие в переводе
- Помогите нам перевести %s на ваш язык
- Создать ярлык, с помощью которого можно открыть parasitic-менеджер (т. е. LSPosed в скрытом режиме)
- Ярлык закреплён
- Текущий лаунчер по умолчанию не поддерживает закрепление ярлыков
- Уведомление о состоянии
- Показывать уведомление, через которое можно открыть parasitic-менеджер (т. е. LSPosed в скрытом режиме)
- Канал обновлений
- Стабильные версии
- Бета-версии
- Еженочные версии
-
- ПрочтиМеня
- Версии
- Информация
- Домашняя страница
- Исходный код
- Разработчики
- Вложения
- Открыть в браузере
- Показать старые версии
- Больше нет релизов
- Ошибка загрузки репозитория модуля: %s
- Сначала обновляемые
- Установлен
-
- - %d скачивание
- - %d скачивания
- - %d скачиваний
- - %d скачиваний
-
-
- Сакура
- Красный
- Розовый
- Фиолетовый
- Насыщенный фиолетовый
- Индиго
- Синий
- Голубой
- Циан
- Сине-зеленый
- Зелёный
- Светло-зелёный
- Лаймовый
- Жёлтый
- Янтарный
- Оранжевый
- Насыщенный оранжевый
- Коричневый
- Серо-голубой
-
diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml
deleted file mode 100644
index d2c0dd74d..000000000
--- a/app/src/main/res/values-si/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- දළ විශ්ලේෂණය
- මොඩියුල
-
- - %d මොඩියුලය සක්රිය කරන්න
- - %d මොඩියුල සබල කර ඇත
-
- සටහන්
- සැකසීම
- ෆිඩ්බැක් ඔ යෝජනා
- මේ ගැන
- ගැටලුවක් රිපෝට් කර්න්න
- ගබඩාව
- සියලු මොඩියුල අප්ඩේට් ය
- %s හි ප්රකාශයට පත් කරන ලදී
- %sට අප්ඩේට් කරන ලදී
-
- - %d මොඩියුලය උත්ශ්රේණි කළ හැකිය
- - %d මොඩියුල උත්ශ්රේණි කළ හැකිය
-
- සම්බන්ධ වන්න %2$s]]>
- null
- ඉන්ට්ට්රොල්
- LSPosed ස්ථාපනය කිරීමට තට්ටු කරන්න
- ස්ථාපනය කර නැත
- LSPosed ස්ථාපනය කර නැත
- සක්රිය කර ඇත
- බාගෙට ඇක්ටිව් කර ඇත
- SEPolicy හරියට ලෝඩ් වෙන් නෑ
- කරුණාකර මෙය Magisk ඩිවලෝපර් වෙත රිපෝට් කරන්න.]]>
- පද්ධති ෆ්රෙම්වවර් ඉන්ජෙක්ෂන් ෆෙල්ඩ්
- Magisk හෝ සමහර අඩු ගුණාත්මක Magisk මොඩියුල නිසා ඇති විය හැක. කරුණාකර Riru සහ LSPosed හැර අනෙකුත් Magisk මොඩියුල අක්රිය කිර උත්සාහ කරන්න හෝ ඩිවලොපර්ස්ලට සම්පූර්ණ ලොගය ඉදිරිපත් කරන්න.]]>
- පද්ධති ප්රෝප් වැරදියි
- මොඩියුල වරින් වර අවලංගු විය හැක.]]>
- අප්ඩේට් කිරීම අවශ්ය වේ
- කරුණාකර LSPposed හි නවතම අනුවාදය ස්ථාපනය කරන්න
- API අනුවාදය
- ෆ්රෙම්වර්ක් අනුවාදය
- කළමනාකරු පැකේජයේ නම
- පද්ධති අනුවාදය
- උපාංගය
- පද්ධති ABI
- ඩෙක්ස් ඔප්ටිමයිසේර් ව්රප්පර්
- සක්රීය කර ඇත
- සක්රීය කර නැත
- සහාය දුන්නා
- සහාය නොදක්වයි
- Android අනුවාදය තෘප්තිමත් නොවේ
- කඩා වැටුණා
- සවි කිරීම අසාර්ථක විය
- SELinux අවසර ඇත
- SELinux ප්රතිපත්තිය වැරදියි
- LSPposed යාවත්කාලීන කරන්න
- LSPposed යාවත්කාලීන කිරීම තහවුරු කරන්නද? යාවත්කාලීන කිරීම අවසන් වූ පසු මෙම උපාංගය නැවත ආරම්භ වේ
- පසුරු පුවරුවට පිටපත් කර ඇත
-
- LSPposed වෙත සාදරයෙන් පිළිගනිමු
- ඔබ කෙටිමඟ නිර්මාණය කළ හැකි හෝ තවමත් දැනුම්දීමෙන් විවෘත කළ හැකි පරපෝෂිත කළමනාකරු භාවිත කරයි.
- දැනුම්දීමකින් විවෘත කළ හැකි පරපෝෂිත කළමනාකරු ඔබ භාවිත කරයි.
- කෙටිමං සාදන්න
- කවදාවත් පෙන්වන්න එපා
- පරපෝෂිත කළමනාකරු නිර්දේශ කෙරේ
- LSPosed දැන් හඳුනාගැනීම වැළැක්වීම සඳහා පද්ධති පරපෝෂිතකරණයට සහය දක්වයි, ඔබට දැනුම්දීමෙන් පරපෝෂිත කළමනාකරු විවෘත කළ හැක. වත්මන් යෙදුම අස්ථාපනය කිරීම නිර්දේශ කෙරේ.
-
- සුරකින්න
- වාචික ලඝු-සටහන්
- මොඩියුල ලොග්
- ලොගය සුරකිමින්, කරුණාකර රැඳී සිටින්න
- ලඝු-සටහන් සුරකින ලදී
- සුරැකීමට අසමත් විය:\n%s
- දැන් ලොගය හිස් කරන්න
- ලොගය සාර්ථකව හිස් කරන ලදී.
- ඉහළට අනුචලනය කරන්න
- …පූරණය වේ
- පහළට අනුචලනය කරන්න
- නැවත පූරණය කරන්න
- ලොගය හිස් කිරීමට අසමත් විය
- වචන එතුම
- වාචික ලොගය සක්රීය කර ඇත
- වාචික ලොගය අක්රීය කර ඇත
-
- (විස්තරයක් සපයා නැත)
- මෙම මොඩියුලයට නව Xposed අනුවාදයක් අවශ්ය වේ (%d) එබැවින් සක්රිය කළ නොහැක
- මෙම මොඩියුලය නවතම Xposed අනුවාදයක් සඳහා නිර්මාණය කර ඇත (%d) එබැවින් සමහර ක්රියාකාරීත්වයන් ක්රියා නොකරනු ඇත
- මෙම මොඩියුලය එයට අවශ්ය Xposed අනුවාදය සඳහන් නොකරයි.
- මෙම මොඩියුලය Xposed අනුවාදය %1$dසඳහා නිර්මාණය කරන ලද නමුත් %2$dඅනුවාදයේ නොගැලපෙන වෙනස්කම් හේතුවෙන් එය අක්රීය කර ඇත
- මෙම මොඩියුලය SD පතෙහි ස්ථාපනය කර ඇති නිසා එය පූරණය කළ නොහැක, කරුණාකර එය අභ්යන්තර ආචයනය වෙත ගෙන යන්න
- අස්ථාපනය කරන්න
- මොඩියුල සැකසුම්
- Repo හි බලන්න
- ඔබට මෙම මොඩියුලය අස්ථාපනය කිරීමට අවශ්යද?
- අස්ථාපනය %1$s
- අස්ථාපනය අසාර්ථකයි
- පරිශීලකයාට මොඩියුලය එක් කරන්න
- පරිශීලක %2$sවෙත %1$s එකතු කරන ලදී
- මොඩියුලය එකතු කිරීම අසාර්ථක විය
- පරිශීලක %sවෙත ස්ථාපනය කරන්න
- පරිශීලක %2$sවෙත %1$s ස්ථාපනය කිරීමට අවශ්යද? අතින් ස්ථාපනය කිරීම රෙකමදාරු කරනු ලැබේ, LSPosed හරහා බලහත්කාරයෙන් ස්ථාපනය කිරීම ගැටළු ඇති කළ හැකිය.
- පුළුල් කරන්න
- කඩා වැටීම
-
- නැවත ප්රශස්ත කරන්න
- ප්රශස්තකරණය…
- ප්රශස්තකරණය සම්පූර්ණයි
- එය දියත් කරන්න
- ප්රශස්තකරණය අසාර්ථක විය: ආපසු ලැබෙන අගය හිස්ය
- ප්රශස්තකරණය අසාර්ථක විය:
- අයදුම්පත් නම
- පැකේජයේ නම
- කාලය ස්ථාපනය කරන්න
- යාවත්කාලීන කාලය
- ආපසු හැරවීම
- පද්ධති යෙදුම්
- වර්ග කිරීම
- මොඩියුලය සක්රිය කරන්න
- ඔබ කිසිදු යෙදුමක් තෝරාගෙන නැත. දිගටම කරගෙන යන්නද?
- ක්රීඩා
- මොඩියුල
- විෂය පථ ලැයිස්තුව සුරැකීමට අසමත් විය
- අනුවාදය: %1$s
- නිර්දේශ කර ඇත
- ඔබ කිසිදු යෙදුමක් තෝරාගෙන නැත. නිර්දේශිත යෙදුම් තෝරන්නද?
- නිර්දේශිත යෙදුම් තෝරන්නද?
- Xposed මොඩියුලය තවම සක්රිය කර නැත
- නිර්දේශ කර ඇත
- යාවත්කාලීන ලබා ගත හැක: %1$s
- යෙදුමක් තෝරා නොගත් නිසා මොඩියුලය %s අබල කර ඇත.
- පද්ධති රාමුව
- උපස්ථ
- උපස්ථ
- ප්රතිෂ්ඨාපනය කරන්න
- බලෙන් නවත්වන්න
- බලෙන් නතර කරන්නද?
- ඔබ යෙදුමක් නැවැත්වීමට බල කළහොත්, එය වැරදි ලෙස හැසිරිය හැක.
- මෙම වෙනස යෙදීම සඳහා නැවත පණගැන්වීම අවශ්ය වේ
- නැවත ආරම්භ කරන්න
- සඟවන්න
-
- වෙනත් යෙදුමක බලන්න
- යෙදුම් තොරතුරු
- ¯\\\\_(ツ)_\/¯\nමෙහි කිසිවක් නැත
-
- රාමුව
- වාචික ලඝු-සටහන් අක්රීය කරන්න
- වාචික ලඝු-සටහන් ඇතුළත් කිරීමට ගැටළු ඉල්ලීම් වාර්තා කරන්න
- කළු අඳුරු තේමාව
- අඳුරු තේමාව සබල කර ඇත්නම් පිරිසිදු කළු තේමාව භාවිත කරන්න
- තේමාව
- උපස්ථ සහ නැවත ස්ථාපිත
- උපස්ථ මොඩියුල ලැයිස්තුව සහ විෂය පථ ලැයිස්තු.
- මොඩියුල ලැයිස්තුව සහ විෂය පථ ලැයිස්තු ප්රතිසාධනය කරන්න.
- උපස්ථ
- උපස්ථ කිරීමට අසමත් විය:\n%s
- කරුණාකර DocumentUI සබල කරන්න
- ප්රතිෂ්ඨාපනය කරන්න
- ප්රතිසාධනය කිරීමට අපොහොසත් විය:\n%s
- ජාල
- HTTPS හරහා DNS
- සමහර රටවල ඩීඑන්එස් විෂ වීම සඳහා විසඳුම්
- තේමා වර්ණය
- පද්ධති තේමා වර්ණය
- දියත් කිරීමේ අයිකන පෙන්වීමට යෙදුම් බල කරන්න
- Android 10 ට පසුව, යෙදුම් වලට ඔවුන්ගේ දියත් කිරීමේ අයිකන සැඟවීමට ඉඩ නොදේ. මෙම පද්ධති විශේෂාංගය අක්රිය කිරීමට ටොගලය අක්රිය කරන්න.
- පද්ධති
- භාෂාව
- පරිවර්තන දායකයින්
- පරිවර්තනයට සහභාගී වන්න
- %s ඔබේ භාෂාවට පරිවර්තනය කිරීමට අපට උදවු කරන්න
- පරපෝෂිත කළමනාකරු විවෘත කළ හැකි කෙටිමඟක් සාදන්න
- කෙටිමඟ අමුණා ඇත
- වත්මන් පෙරනිමි දියත් කිරීම pin කෙටිමං සඳහා සහය නොදක්වයි
- තත්ත්ව දැනුම්දීම
- පරපෝෂිත කළමනාකරු විවෘත කළ හැකි දැනුම්දීමක් පෙන්වන්න
- නාලිකාව යාවත්කාලීන කරන්න
- ස්ථාවර
- බීටා
- රාත්රී ගොඩනැගීම
-
- කියවන්න
- නිකුත් කරයි
- තොරතුරු
- මුල් පිටුව
- ප්රභව කේතය
- සහයෝගිතාකරුවන්
- වත්කම්
- බ්රවුසරයේ විවෘත කරන්න
- පැරණි අනුවාද පෙන්වන්න
- තවත් නිදහස් කිරීමක් නැත
- මොඩියුල repo: %sපූරණය කිරීමට අසමත් විය
- පළමුව උසස් කළ හැකිය
- ස්ථාපනය කර ඇත
-
- - %d බාගැනීම
- - බාගැනීම් %d
-
-
- සකුරා
- රතු
- රෝස
- දම් පාට
- තද දම්
- ඉන්ඩිගෝ
- නිල්
- ලා නිල්
- සියන්
- ටීල්
- කොළ
- ලා කොළ
- දෙහි
- කහ
- ඇම්බර්
- දොඩම්
- තද තැඹිලි
- දුඹුරු
- නිල් අළු
-
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
deleted file mode 100644
index e6ce840da..000000000
--- a/app/src/main/res/values-sk/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- Prehľad
- Moduly
-
- - %d modul povolený
- - %d povolené moduly
- - %d povolené moduly
- - %d povolené moduly
-
- Protokoly
- Nastavenia
- Spätná väzba alebo návrh
- O stránke
- Nahlásiť problém
- Úložisko
- Aktualizácia všetkých modulov
- Zverejnené na %s
- Aktualizované na %s
-
- - %d modul s možnosťou aktualizácie
- - %d moduly s možnosťou aktualizácie
- - %d moduly s možnosťou aktualizácie
- - %d moduly s možnosťou aktualizácie
-
- Pripojte sa k nášmu kanálu %2$s]]>
- null
- Inštalácia stránky
- Ťuknutím na položku nainštalujete LSPosed
- Nie je nainštalovaný
- LSPosed nie je nainštalovaný
- Aktivované
- Čiastočne aktivované
- SEPolicy sa nenačíta správne
- Nahláste to vývojárovi Magisk .]]>
- Vstreknutie systémového rámca zlyhalo
- Magisk alebo niektorými nekvalitnými modulmi Magisk. Skúste vypnúť iné moduly Magisk ako Riru a LSPosed alebo pošlite vývojárom celý log.]]>
- Nesprávna opora systému
- Moduly sa môžu občas znefunkčniť.]]>
- Je potrebné aktualizovať
- Nainštalujte si najnovšiu verziu aplikácie LSPosed
- Verzia API
- Verzia rámca
- Názov balíka správcu
- Verzia systému
- Zariadenie
- Systém ABI
- Dex Optimizer Wrapper
- Povolené
- Nie je povolené
- Podporované stránky
- Nepodporované
- Verzia pre Android nespokojná
- Havarovaný
- Montáž zlyhala
- SELinux je permisívny
- Zásady systému SELinux sú nesprávne
- Aktualizácia LSPosed
- Potvrdiť aktualizáciu LSPosed? Toto zariadenie sa po dokončení aktualizácie reštartuje
- Skopírované do schránky
-
- Vitajte v LSPosed
- Používate parazitného správcu, ktorý môže vytvoriť zástupcu alebo ho stále otvárať z oznámenia.
- Používate parazitného správcu, ktorý sa môže otvoriť z oznámenia.
- Vytvorenie skratky
- Nikdy sa neukážte
- Odporúčaný parazitický manažér
- LSPosed teraz podporuje parazitovanie systému, aby sa zabránilo detekcii, môžete otvoriť správcu parazitov z oznámenia. Odporúča sa odinštalovať aktuálnu aplikáciu.
-
- Uložiť
- Verbálne protokoly
- Protokoly modulov
- Ukladanie denníka, počkajte, prosím
- Uložené protokoly
- Nepodarilo sa uložiť:\n%s
- Vymazať protokol
- Protokol bol úspešne vymazaný.
- Prejdite na začiatok
- Načítavanie…
- Prejdite na spodok
- Znovunačítanie
- Nepodarilo sa vymazať protokol
- Word Wrap
- Verbose log povolený
- Verbose log vypnuté
-
- (bez popisu)
- Tento modul vyžaduje novšiu verziu Xposed (%d), a preto ho nemožno aktivovať
- Tento modul je určený pre novšiu verziu Xposed (%d), a preto niektoré funkcie nemusia fungovať
- Tento modul neuvádza verziu Xposed, ktorú potrebuje.
- Tento modul bol vytvorený pre verziu Xposed %1$d, ale kvôli nekompatibilným zmenám vo verzii %2$d, bol vypnutý.
- Tento modul nie je možné načítať, pretože je nainštalovaný na karte SD, prosím, presuňte ho do internej pamäte
- Odinštalovanie stránky
- Nastavenia modulu
- Zobraziť v časti Repo
- Chcete tento modul odinštalovať?
- Odinštalovaná stránka %1$s
- Odinštalovanie neúspešné
- Pridanie modulu k používateľovi
- Pridanie stránky %1$s do používateľa %2$s
- Pridanie modulu zlyhalo
- Inštalácia do používateľa %s
- Chcete nainštalovať stránku %1$s na používateľa %2$s? Odporúča sa inštalovať ručne, vynútená inštalácia cez LSPosed môže spôsobiť problémy.
- rozšíriť
- zrútenie
-
- Opätovná optimalizácia
- Optimalizácia…
- Optimalizácia dokončená
- Spustite ho
- Optimalizácia zlyhala: návratová hodnota je prázdna
- Optimalizácia zlyhala:
- Názov aplikácie
- Názov balíka
- Čas inštalácie
- Čas aktualizácie
- Reverzný
- Systémové aplikácie
- Triedenie
- Povolenie modulu
- Nevybrali ste žiadnu aplikáciu. Pokračovať?
- Hry
- Moduly
- Nepodarilo sa uložiť zoznam oblastí
- Verzia: %1$s
- Odporúčané stránky
- Nevybrali ste žiadnu aplikáciu. Vybrať odporúčané aplikácie?
- Vybrať odporúčané aplikácie?
- Modul Xposed ešte nie je aktivovaný
- Odporúčané stránky
- Dostupná aktualizácia: %1$s
- Modul %s bol vypnutý, pretože nebola vybraná žiadna aplikácia.
- Systémový rámec
- Zálohovanie
- Zálohovanie
- Obnovenie
- Vynútiť zastavenie
- Zastaviť?
- Ak aplikáciu násilne zastavíte, môže sa správať nesprávne.
- Na uplatnenie tejto zmeny je potrebný reštart
- Reštart
- Skryť
-
- Zobraziť v inej aplikácii
- Informácie o aplikácii
- \\\\_(ツ)_\/¯\nTu nič nie je
-
- Rámec
- Zakázať slovné protokoly
- Žiadosť o nahlásenie problémov, aby obsahovala slovné protokoly
- Čierna tmavá téma
- Použitie čisto čiernej témy, ak je povolená tmavá téma
- Téma
- Zálohovanie a obnovenie
- Záložný zoznam modulov a zoznamy rozsahov.
- Obnovenie zoznamu modulov a zoznamov oblastí.
- Zálohovanie
- Nepodarilo sa zálohovať:\n%s
- Povoľte DocumentUI
- Obnovenie
- Nepodarilo sa obnoviť:\n%s
- Sieť
- DNS cez HTTPS
- Obchádzanie otravy DNS v niektorých krajinách
- Farba témy
- Farba témy systému
- Vynútenie aplikácií, aby zobrazovali ikony spúšťača
- Po systéme Android 10 aplikácie nemôžu skrývať ikony spúšťača. Vypnutím prepínača túto systémovú funkciu zakážete.
- Systém
- Jazyk
- Spolupracovníci na preklade
- Zúčastnite sa na preklade
- Pomôžte nám preložiť stránku %s do vášho jazyka
- Vytvorenie skratky, ktorá dokáže otvoriť parazitného správcu
- Skratka pripnutá
- Súčasný predvolený spúšťač nepodporuje pripnuté skratky
- Oznámenie o stave
- Zobrazenie oznámenia, ktoré môže otvoriť parazitický správca
- Aktualizačný kanál
- Stabilné
- Beta
- Nočné zostavenie
-
- Readme
- Uvoľňuje
- Informácie
- Domovská stránka
- Zdrojový kód
- Spolupracovníci
- Aktíva
- Otvoriť v prehliadači
- Zobrazenie starších verzií
- Žiadne ďalšie uvoľňovanie
- Nepodarilo sa načítať modul repo: %s
- Najskôr aktualizovateľné
- Nainštalované
-
- - %d stiahnuť
- - %d Na stiahnutie
- - %d Na stiahnutie
- - %d Na stiahnutie
-
-
- Sakura
- Červená
- Ružová
- Fialová
- Tmavo fialová
- Indigo
- Modrá
- Svetlomodrá
- Cyan
- Teal
- Zelená
- Svetlozelená
- Vápno
- Žltá
- Amber
- Oranžová
- Sýto oranžová
- Hnedá
- Modrá šedá
-
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
deleted file mode 100644
index 2b683674f..000000000
--- a/app/src/main/res/values-sv/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- Översikt
- Moduler
-
- - %d moduler aktiverade
- - %d moduler aktiverade
-
- Loggar
- Inställningar
- Återkoppling eller förslag
- Om
- Rapportera problem
- Förvar
- Alla moduler uppdaterade
- Publicerad på %s
- Uppdaterad på %s
-
- - %d modul uppgraderbar
- - %d moduler uppgraderbara
-
- Gå med i vår %2$s kanal]]>
- Anton J
- Installera
- Tryck för att installera LSPosed
- Inte installerad
- LSPosed är inte installerat
- Aktiverad
- Delvis aktiverad
- SEPolicy är inte korrekt laddat
- Vänligen rapportera detta till Magisk utvecklaren.]]>
- Injektion i System Framework misslyckades
- Magisk eller några Magisk-moduler av låg kvalitet. Försök att inaktivera andra Magisk-moduler än Riru och LSPosed eller skicka fullständig logg till utvecklare.]]>
- Felaktig System prop
- Moduler kan komma att ogiltigförklara.]]>
- Behöver uppdateras
- Vänligen installera den senaste versionen av LSPosed
- Tips för utvecklare av moduler
- Inaktivera optimeringar för distribution i Android Studio, eller använd kommandot `gradlew installDebug` för att installera. Annars kommer modulen apk inte att uppdateras.
- API version
- Ramverk version
- Namn på managerpaket
- Systemversion
- Enhet
- System ABI
- Dex Optimizer Wrapper
- Aktiverad
- Inte aktiverad
- Stöds
- Ej stödd
- Android-version otillfredsställd
- Kraschade
- Montering misslyckades
- SELinux är tillåtande
- SELinux-politiken är felaktig
- Uppdatera LSPosed
- Bekräfta att uppdatera LSPosed? Denna enhet kommer att starta om efter att uppdateringen är slutförd
- Kopieras till urklipp
-
- Välkommen till LSPosed
- Du använder Parasitic Manager, som kan skapa genvägar eller fortfarande öppna från anmälan.
- Du använder Parasitic Manager, som kan öppnas från en avisering.
- Skapa genväg
- Visa aldrig
- Parasitic Manager Rekommenderad
- LSPosed stöder nu systemparasitering för att undvika upptäckt, du kan öppna parasithanteraren från anmälan. Det rekommenderas att avinstallera det aktuella programmet.
-
- Spara
- Utförliga Loggar
- Modul-loggar
- Sparar logg, vänligen vänta
- Loggar som sparas
- Det gick inte att spara:\n%s
- Rensa loggen nu
- Loggen har rensats.
- Rulla till toppen
- Laddar…
- Rulla till botten
- Reload
- Det gick inte att rensa loggen
- Ord Wrap
- Verbose logg aktiverad
- Verbose logg inaktiverad
-
- (ingen beskrivning anges)
- Denna modul kräver en nyare Xposed version (%d) och kan därför inte aktiveras
- Den här modulen är utformad för en nyare Xposed-version (%d) och därför kan det hända att vissa funktioner inte fungerar.
- Modulen anger inte den Xposed version som den behöver.
- Denna modul skapades för Xposed version %1$d, men på grund av inkompatibla förändringar i version %2$dhar den inaktiverats
- Modulen kan inte laddas eftersom den är installerad på SD-kortet, vänligen flytta den till internt lagringsutrymme
- Avinstallera
- Inställningar för modul
- Visa i Repo
- Vill du avinstallera denna modul?
- Avinstallerade %1$s
- Avinstallationen misslyckades
- Lägg till modul till användare
- Lade till %1$s till användare %2$s
- Det gick inte att lägga till modul
- Installera till användare %s
- Vill du installera %1$s till användare %2$s? Det rekommenderas att installera manuellt, tvinga installation via LSPosed kan orsaka problem.
- expandera
- kollapsa
-
- Återoptimera
- Optimerar…
- Optimering slutförd
- Starta den
- Optimering misslyckades: returvärdet är tomt
- Optimering misslyckades:
- Programmets namn
- Paketets namn
- Installera tid
- Uppdatera tid
- Omvänd
- System appar
- Sortering
- Aktivera modul
- Du valde inte någon app. Fortsätt?
- Spel
- Moduler
- Det gick inte att spara omfattningslistan
- Version: %1$s
- Välj
- Rekommenderad
- Du valde inte någon app. Välj rekommenderade appar?
- Välj rekommenderade appar?
- Alla
- Ingen
- Automatisk inkludering
- Xposed modul är inte aktiverad än
- Rekommenderad
- Uppdatering tillgänglig: %1$s
- Modulen %s har inaktiverats sedan ingen app har valts.
- System ramverk
- Säkerhetskopiera
- Säkerhetskopiera
- Återställ
- Tvinga avslutad
- Tvinga avslut?
- Om du tvingar en app att sluta fungerar det inte.
- Omstart krävs för att denna ändring ska gälla
- Reboot
- Dölj
-
- Visa i annan app
- Information om appen
- <unk> \\\\_(<unk> )_\/ <unk>\nIngenting här
-
- Framework
- Inaktivera utförliga loggar
- Begäran om att inkludera verbose-loggar i rapporten
- Svart mörkt tema
- Använd det rena svarta temat om mörkt tema är aktiverat
- Tema
- Säkerhetskopiera och återställ
- Säkerhetskopiera modullista och omfattningslistor.
- Återställ modullista och omfattningslistor.
- Säkerhetskopiera
- Det gick inte att säkerhetskopiera:\n%s
- Aktivera DocumentUI
- Återställ
- Det gick inte att återställa:\n%s
- Nätverk
- DNS över HTTPS
- Workaround DNS-förgiftning i vissa nationer
- Tema färg
- Färg på systemtema
- Tvinga appar att visa launcher-ikoner
- Efter Android 10, appar är inte tillåtet att dölja sina launcher ikoner. Stäng av växla för att inaktivera den här systemfunktionen.
- System
- Språk
- Bidragande översättare
- Delta i översättning
- Hjälp oss att översätta %s till ditt språk
- Skapa en genväg som kan öppna Parasitic Manager
- Genväg fastlåst
- Den nuvarande standardstartaren har inte stöd för genvägar med stift
- Meddelande om status
- Visa en anmälan som kan öppna parasitansvarig
- Uppdatera kanal
- Stabil
- Beta
- Nattligt bygg
-
- Läsa
- Släpp
- Information
- Hemsida
- Källkod
- Medarbetare
- Tillgångar
- Öppna i webbläsare
- Visa äldre versioner
- Ingen mer utgåva
- Misslyckades att ladda modul rapo: %s
- Uppgraderingsbar först
- Installerad
-
- - %d ladda ner
- - %d nedladdningar
-
-
- Sakura
- Röd
- Rosa
- Lila
- Djup lila
- Indigo
- Blå
- Ljusblå
- Turkos
- Kricka
- Grön
- Ljusgrön
- Limegrön
- Gul
- Bärnsten
- Orange
- Djup orange
- Brun
- Blå grå
-
diff --git a/app/src/main/res/values-sw600dp/integer.xml b/app/src/main/res/values-sw600dp/integer.xml
deleted file mode 100644
index af5374b7c..000000000
--- a/app/src/main/res/values-sw600dp/integer.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- 0x50
-
-
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
deleted file mode 100644
index 8829d7c7c..000000000
--- a/app/src/main/res/values-th/strings.xml
+++ /dev/null
@@ -1,234 +0,0 @@
-
-
-
-
- ภาพรวม
- โมดูล
-
- - %d โมดูลที่เปิดใช้งาน
-
- บันทึก
- การตั้งค่า
- ข้อเสนอแนะ หรือ คำแนะนำ
- เกี่ยวกับ
- รายงานปัญหา
- คลังโมดูล
- โมดูลทั้งหมดเป็นเวอร์ชั่นล่าสุด
- เผยแพร่เมื่อ %s
- อัปเดตเมื่อ %s
-
- - %d โมดูลที่สามารถอัพเกรดได้
-
- หรือเข้าร่วม %2$s
- ของเรา]]>
- null
- ติดตั้ง
- แตะเพื่อติดตั้ง LSPosed
- ไม่ได้ติดตั้ง
- ไม่ได้ติดตั้ง LSPosed
- เปิดใช้งานแล้ว
- เปิดใช้งานบางส่วน
- การโหลด SEPolicy ไม่ถูกต้อง
- โปรดรายงานที่Magisk developer.]]>
- Injection ระบบ Framework ล้มเหลว
- Magisk หรือโมดูล Magisk ที่มีคุณภาพต่ำบางตัว โปรดลองปิดการใช้งานโมดูล Magisk อื่นที่ไม่ใช่ Riru และ LSPosed หรือส่งบันทึกแบบเต็มไปยังนักพัฒนา]]>
- System prop ไม่ถูกต้อง
- โมดูลอาจใช้ไม่ได้ในบางครั้ง]]>
- จำเป็นต้องอัพเดท
- โปรดติดตั้ง LSPosed . เวอร์ชันล่าสุด
- เวอร์ชัน API
- เวอร์ชัน Framework
- ชื่อแพ็คเกจผู้จัดการ
- เวอร์ชันของระบบ
- อุปกรณ์
- ระบบ ABI
- Dex Optimizer Wrapper
- เปิดใช้งาน
- ไม่ได้เปิดใช้งาน
- ได้รับการสนับสนุน
- ไม่รองรับ
- เวอร์ชัน Android ไม่พอใจ
- พัง
- เมานต์ล้มเหลว
- SELinux ได้รับอนุญาต
- นโยบาย SELinux ไม่ถูกต้อง
- อัพเดท LSPosed
- ยืนยันการอัปเดต LSPosed? อุปกรณ์นี้จะรีบูตหลังจากอัปเดตเสร็จสิ้น
- คัดลอกไปยังคลิปบอร์ดแล้ว
-
- ยินดีต้อนรับสู่ LSPosed
- คุณกำลังใช้ Lsposed ซึ่งสามารถสร้างทางลัดหรือยังคงเปิดจากการแจ้งเตือน
- คุณกำลังใช้ตัวจัดการ Lsposed ซึ่งสามารถเปิดได้จากการแจ้งเตือน
- สร้างทางลัด
- ไม่ต้องแสดง
- แนะนำ กำจัดไวรัส
- LSPosed รองรับการปรสิตของระบบเพื่อหลีกเลี่ยงการตรวจจับ คุณสามารถเปิดโปรแกรมจัดการปรสิตได้จากการแจ้งเตือน ขอแนะนำให้ถอนการติดตั้งแอปพลิเคชันปัจจุบัน
-
- บันทึก
- บันทึกอย่างละเอียด
- บันทึกโมดูล
- กำลังบันทึกบันทึก โปรดรอสักครู่
- บันทึก Log แล้ว
- ไม่สามารถบันทึก:\n%s
- ล้างบันทึกตอนนี้
- ล้างบันทึกเรียบร้อยแล้ว
- เลื่อนขึ้นด้านบน
- กำลังโหลด…
- เลื่อนไปด้านล่าง
- โหลดซ้ำ
- ไม่สามารถล้างบันทึก
- ตัดคำ
- เปิดใช้งานบันทึกอย่างละเอียด
- บันทึกรายละเอียดปิดการใช้งาน
-
- (ไม่มีคำอธิบายให้)
- โมดูลนี้ต้องการเวอร์ชัน Xposed ที่ใหม่กว่า (%d) ดังนั้นจึงไม่สามารถเปิดใช้งานได้
- โมดูลนี้ออกแบบมาสำหรับ Xposed เวอร์ชั่น (%d) ดังนั้นฟังก์ชั่นบางอย่างอาจไม่ทำงาน
- โมดูลนี้ไม่ได้ระบุเวอร์ชัน Xposed ที่ต้องการ
- โมดูลนี้ถูกสร้างขึ้นสำหรับ Xposed เวอร์ชัน %1$dแต่เนื่องจากการเปลี่ยนแปลงที่เข้ากันไม่ได้ในเวอร์ชัน %2$dจึงถูกปิดใช้งาน
- ไม่สามารถโหลดโมดูลนี้ได้เนื่องจากติดตั้งอยู่ในการ์ด SD โปรดย้ายไปยังที่จัดเก็บข้อมูลภายใน
- ถอนการติดตั้ง
- การตั้งค่าโมดูล
- ดูใน Repo
- คุณต้องการถอนการติดตั้งโมดูลนี้หรือไม่?
- ถอนการติดตั้ง %1$s
- ถอนการติดตั้งไม่สำเร็จ
- เพิ่มโมดูลให้กับผู้ใช้
- เพิ่ม %1$s ให้กับผู้ใช้ %2$s
- การเพิ่มโมดูลล้มเหลว
- ติดตั้งให้กับผู้ใช้ %s
- ต้องการติดตั้ง %1$s ให้กับผู้ใช้ %2$sหรือไม่ ขอแนะนำให้ติดตั้งด้วยตนเอง การบังคับติดตั้งผ่าน LSPosed อาจทำให้เกิดปัญหาได้
- ขยาย
- ทรุด
-
- เพิ่มประสิทธิภาพอีกครั้ง
- การเพิ่มประสิทธิภาพ…
- การเพิ่มประสิทธิภาพเสร็จสมบูรณ์
- เปิดตัวมัน
- การเพิ่มประสิทธิภาพล้มเหลว: ค่าที่ส่งคืนว่างเปล่า
- การเพิ่มประสิทธิภาพล้มเหลว:
- ชื่อแอปพลิเคชัน
- ชื่อแพ็คเกจ
- ติดตั้งเวลา
- อัพเดทเวลา
- ย้อนกลับ
- แอประบบ
- การเรียงลำดับ
- เปิดใช้งานโมดูล
- คุณไม่ได้เลือกแอพใด ๆ ดำเนินการต่อ?
- เกม
- โมดูล
- ไม่สามารถบันทึกรายการขอบเขต
- เวอร์ชั่น: %1$s
- ที่แนะนำ
- คุณไม่ได้เลือกแอพใด ๆ เลือกแอพที่แนะนำ?
- เลือกแอพที่แนะนำ?
- โมดูล Xposed ยังไม่ได้เปิดใช้งาน
- ที่แนะนำ
- มีการอัปเดต: %1$s
- โมดูล %s ถูกปิดใช้งานเนื่องจากไม่ได้เลือกแอป
- กรอบงานระบบ
- สำรอง
- สำรอง
- คืนค่า
- บังคับหยุด
- บังคับหยุด?
- หากคุณบังคับหยุดแอป แอปอาจทำงานผิดปกติ
- จำเป็นต้องรีบูตเพื่อให้การเปลี่ยนแปลงนี้มีผล
- รีบูต
- ซ่อน
-
- ดูในแอปอื่น
- ข้อมูลแอพ
- ¯\\\\_(ツ)_\/¯\nไม่มีอะไรที่นี่
-
- กรอบ
- ปิดใช้งานบันทึกอย่างละเอียด
- แจ้งปัญหาขอรวมบันทึกอย่างละเอียด
- ธีมสีดำมืด
- ใช้ธีมสีดำล้วนหากเปิดใช้งานธีมสีเข้ม
- ธีม
- สำรองและเรียกคืน
- รายการโมดูลสำรองและรายการขอบเขต
- คืนค่ารายการโมดูลและรายการขอบเขต
- สำรอง
- การสำรองข้อมูลล้มเหลว:\n%s
- โปรดเปิดใช้งาน DocumentUI
- คืนค่า
- ไม่สามารถกู้คืนได้:\n%s
- เครือข่าย
- DNS ผ่าน HTTPS
- วิธีแก้ปัญหา DNS เป็นขัดข้องในบางประเทศ
- สีของธีม
- สีของชุดรูปแบบของระบบ
- บังคับให้แอปแสดงไอคอนตัวเรียกใช้
- หลังจาก Android 10 แอปจะไม่ได้รับอนุญาตให้ซ่อนไอคอนตัวเรียกใช้งาน ปิดสวิตช์เพื่อปิดใช้งานคุณลักษณะของระบบนี้
- ระบบ
- ภาษา
- ผู้ร่วมแปล
- มีส่วนร่วมในการแปล
- ช่วยเราแปล %s เป็นภาษาของคุณ
- สร้างทางลัดที่สามารถเปิดตัวจัดการปรสิต
- ตรึงทางลัดแล้ว
- Launcher เริ่มต้นปัจจุบันไม่รองรับทางลัด PIN
- การแจ้งเตือนสถานะ
- แสดงการแจ้งเตือนที่สามารถเปิดตัวจัดการปรสิต
- อัพเดทช่อง
- มั่นคง
- เบต้า
- สร้างทุกคืน
-
- ต้องอ่าน
- เผยแพร่
- ข้อมูล
- หน้าแรก
- รหัสแหล่งที่มา
- ผู้ร่วมงาน
- คลัง
- เปิดในเบราว์เซอร์
- แสดงเวอร์ชันเก่า
- ไม่ปล่อยอีกแล้ว
- ไม่สามารถโหลดโมดูล repo: %s
- อัพเกรดก่อน
- ติดตั้งแล้ว
-
- - %d ดาวน์โหลด
-
-
- ซากุระ
- สีแดง
- สีชมพู
- สีม่วง
- สีม่วงเข้ม
- คราม
- สีน้ำเงิน
- ฟ้าอ่อน
- สีฟ้า
- นกเป็ดน้ำ
- สีเขียว
- สีเขียวอ่อน
- มะนาว
- สีเหลือง
- อำพัน
- ส้ม
- ส้มเข้ม
- สีน้ำตาล
- น้ำเงินเทา
-
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
deleted file mode 100644
index 0329c356d..000000000
--- a/app/src/main/res/values-tr/strings.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
- Genel bakış
- Modüller
-
- - %d modül etkin
- - %d modüller etkin
-
- Günlükler
- Ayarlar
- Geri bildirim veya öneri
- Hakkında
- Hata bildir
- Modül deposu
- Bütün modüller güncel
- %s yayınlandı
- %s güncellendi
-
- - %d modülü güncellenebilir
- - %d modülleri güncellenebilir
-
- %2$s kanalımıza katılın]]>
- Bedircan AYAYDIN, Furkan KARCIOĞLU, Ahmet AÇIKGÖZ, Sir-C
- Yükle
- LSPosed\'i yüklemek için dokunun
- Yüklenmedi
- LSPosed Kurulu değil
- Aktif edildi
- Kısmen etkinleştirildi
- SEPolicy düzgün yüklenmemiş
- Lütfen bunu Magisk geliştiricisine bildirin.]]>
- Sistem Çerçevesi enjeksiyonu başarısız oldu
- Magisk veya düşük kaliteli modüller yüzünden olabilir. Lütfen Riru ve LSPosed dışında bütün modülleri devre dışı bırakmayı deneyin veya geliştiricilere tam bir hata kaydı gönderin.]]>
- Sistem desteği yanlış
- Modüller zaman zaman düzgün çalışmayabilir.]]>
- Güncelleme gerekiyor
- Lütfen LSPosed\'in en son sürümünü yükleyin
- Modül geliştiricisi için ipuçları
- Lütfen Android Studio\'da dağıtım optimizasyonlarını devre dışı bırakın veya yüklemek için `gradlew installDebug` komutunu kullanın. Aksi takdirde modül apk\'sı güncellenmeyecektir.
- API sürümü
- Çerçeve sürümü
- Yönetici paket adı
- Sistem sürümü
- Cihaz
- Sistem ABI\'si
- Dex Optimize Edici Sarıcısı
- Etkin
- Etkin değil
- Desteklenen
- Desteklenmiyor
- Android sürümü tatmin edici değil
- Çöktü
- Bağlama başarısız
- SELinux\'a izin verildi
- SELinux politikası yanlış
- LSPosed\'ı güncelle
- LSPosed\'ı güncellemek istediğinize emin misiniz? Bu cihaz güncelleme tamamlandıktan sonra yeniden başlatılacak
- Panoya kopyalandı
-
- LSPosed\'a hoş geldiniz
- Parazit yöneticiyi kullanıyorsunuz, bu da kısayol oluşturabileceğinizi veya hala bildirimden açabileceğinizi gösterir.
- Parazit yöneticiyi kullanıyorsunuz ve bu da hala bildirimden açabileceğinizi gösterir.
- Kısayol oluştur
- Asla gösterme
- Parazit Yönetici Önerilir
- LSPosed tespit edilmeyi önlemek için sistem parazitlemeyi destekliyor, parazit yöneticiyi bildirimden açabilirsiniz. Şu anki uygulamayı kaldırmanız önerilir.
-
- Kaydet
- Ayrıntılı Günlükler
- Modül Günlükleri
- Günlük kaydediliyor, lütfen bekleyin
- Günlükler kaydedildi
- Kaydedilemedi:\n%s
- Günlüğü şimdi temizle
- Günlük başarıyla temizlendi.
- Yukarı kaydır
- Yükleniyor…
- Aşağı kaydır
- Tekrar yükle
- Günlük temizlenemedi
- Yeni Satıra Geç
- Ayrıntılı Günlükler Aktif
- Ayrıntılı Günlükler Deaktif
-
- (açıklama yapılmadı)
- Bu modül daha yeni bir Xposed sürümü gerektiriyor (%d) ve bu nedenle etkinleştirilemez
- Bu modül daha yeni bir Xposed sürümü (%d) için tasarlanmıştır ve bu nedenle bazı işlevler çalışmayabilir
- Bu modül, ihtiyaç duyduğu Xposed sürümünü belirtmiyor.
- Bu modül Xposed sürümü için oluşturuldu %1$d, ancak sürümdeki uyumsuz değişiklikler nedeniyle %2$d, devre dışı bırakıldı
- Bu modül yüklenemiyor çünkü SD kartta yüklü, lütfen dahili depolama birimine taşıyın veya SD kart erişimine izin verin
- Kaldır
- Modül ayarları
- Repo\'da görüntüle
- Bu modülü kaldırmak istiyor musunuz?
- %1$s kaldırıldı
- Kaldırma başarısız
- Kullanıcıya modül ekle
- Kullanıcıya %1$s eklendi %2$s
- Modül eklenemedi
- Kullanıcıya yükle %s
- %2$s kullanıcısına %1$s yüklemek ister misiniz? Elle kurulum yapılması tavsiye edilir, LSPosed ile kurulumu zorlamak sorunlara neden olabilir.
- genişlet
- daralt
-
- Yeniden optimize et
- Optimize ediliyor…
- Optimizasyon tamamlandı
- Başlat
- Optimizasyon başarısız oldu: dönüş değeri boş
- Optimizasyon başarısız oldu:
- Uygulama Adı
- Paket ismi
- Yükleme zamanı
- Güncelleme zamanı
- Ters
- Sistem uygulamaları
- Sıralama
- Modülü etkinleştirin
- Herhangi bir uygulama seçmediniz. Devam et?
- Oyunlar
- Modüller
- Kapsam listesi kaydedilemedi
- Sürüm: %1$s
- Seç
- Önerilen
- Herhangi bir uygulama seçmediniz. Önerilen uygulamalar seçilsin mi?
- Önerilen uygulamalar seçilsin mi?
- Hepsi
- Hiçbiri
- Otomatik Dahil Et
- Xposed modülü henüz etkinleştirilmedi
- Önerilen
- Güncelleme uygun: %1$s
- Hiçbir uygulama seçilmediği için %s modülü devre dışı bırakıldı.
- Sistem Çerçevesi
- Yedekleme
- Yedekleme
- Geri yükle
- Durmaya zorla
- Zorla durdurulsun mu?
- Bir uygulamayı durdurmaya zorlarsanız, hatalı davranabilir.
- Bu değişikliğin geçerli olması için yeniden başlatma gerekiyor
- Yeniden başlat
- Gizle
-
- Diğer uygulamada görüntüle
- Uygulama bilgisi
- ¯\\\\_(ツ)_\/¯\nModül bulunamadı
-
- Çerçeve
- Ayrıntılı günlükleri devre dışı bırak
- Ayrıntılı günlükleri dahil etme isteğini raporlayın
- Siyah koyu tema
- Koyu tema etkinse saf siyah temayı kullanın
- Tema
- Yedekle
- Modül ve kapsam listelerini yedekle.
- Modül ve kapsam listelerini geri yükle.
- Yedek
- Yedekleme başarısız oldu:\n%s
- Lütfen DocumentUI\'yi etkinleştirin
- Geri yükle
- Geri yüklenemedi:\n%s
- Ağ
- HTTPS üzerinden DNS
- Bazı ülkelerde DNS zehirlenmesi için geçici çözümdür
- Tema rengi
- Sistem tema rengi
- Uygulamaları başlatıcı simgelerini göstermeye zorla
- Android 10\'dan sonra, uygulamaların başlatıcı simgelerini gizlenmesine izin verilmez. Bu sistem özelliğini devre dışı bırakmak için düğmeyi kapatın.
- Sistem
- Dil
- Çeviri katılımcıları
- Çeviriye katılın
- %s yazılımını çevirmemize yardım edin
- Parazit yöneticiyi açmak için bir kısayol oluşturun
- Ayarlar_kestirme_sabitlenmis
- Geçerli varsayılan başlatıcı, pin kısayollarını desteklemiyor
- Durum Bildirimi
- Parazit yöneticiyi açabilmek için bir bildirim göster
- Güncelleme kanalı
- Stabil
- Beta
- Gecelik
-
- Beni oku
- Sürümler
- Bilgi
- Ana Sayfa
- Kaynak kodu
- Ortak çalışanlar
- Dosyalar
- Tarayıcıda aç
- Eski sürümleri göster
- Daha fazla sürüm yok
- Modül deposu yüklenemedi: %s
- Önce yükseltilebilir
- Kurulmuş
-
- - %d İndir
- - %d indirmeler
-
-
- sakura
- Kırmızı
- Pembe
- Mor
- Koyu mor
- Çivit
- Mavi
- Açık mavi
- Cam göbeği
- Turkuaz
- Yeşil
- Açık yeşil
- Limon
- Sarı
- Kehribar
- Turuncu
- Koyu turuncu
- Kahverengi
- Mavi gri
-
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
deleted file mode 100644
index 409761e1e..000000000
--- a/app/src/main/res/values-uk/strings.xml
+++ /dev/null
@@ -1,248 +0,0 @@
-
-
-
-
- Огляд
- Модулі
-
- - %d модуль увімкнено
- - %d модулі увімкнено
- - %d модулів увімкнено
- - %d модулів увімкнено
-
- Звіти
- Налаштування
- Зворотний зв\'язок або пропозиція
- Детальніше
- Повідомити про проблему
- Репозиторій
- Модулі не потребують оновлення
- Опубліковано о %s
- Оновлено о %s
-
- - %d модуль потребує оновлення
- - %d модулі потребують оновлення
- - %d модулів потребують оновлення
- - %d модулів потребують оновлення
-
- Приєднуйтесь до нашого каналу в %2$s]]>
- AlexPechkin
- Встановити
- Натисніть, щоб встановити LSPosed
- Не встановлено
- LSPosed не встановлено
- Активовано
- Частково активовано
- SEPolicy не завантажено належним чином
- Будь ласка, повідомте про це на Magisk розробнику.]]>
- Помилка ін\'єкції System Framework
- Magisk або деякими неякісними модулями Magisk. Спробуйте вимкнути всі модулі Magisk, крім Riru та LSPosed, або надішліть повний звіт розробникам.]]>
- Неправильні системні налаштування
- Модулі можуть не працювати належним чином.]]>
- Потрібно оновити
- Будь ласка, встановіть останню версію LSPosed
- Поради розробнику модулів
- Будь ласка, відключіть оптимізацію розгортання в Android Studio або використовуйте команду `gradlew installDebug` для встановлення. В іншому випадку apk-модуль не оновиться.
- Версія API
- Версія Framework
- Ім\'я пакета менеджера
- Версія Android
- Пристрій
- Розрядність процесора
- Обгортка Dex Optimizer
- Увімкнено
- Вимкнено
- Підтримується
- Не підтримується
- Версія Android не підтримується
- Помилка
- Не вдалося під\'єднати
- SELinux є дозволеним
- Політика SELinux неправильна
- Оновити LSPosed
- Підтвердити оновлення LSPosed? Цей пристрій перезавантажиться після завершення оновлення
- Скопійовано до буфера обміну
-
- Ласкаво просимо до LSPosed
- Ви використовуєте паразитний менеджер, який може створювати ярлик або все одно відкриватися з повідомлення.
- Ви використовуєте паразитний менеджер, який може відкриватися з повідомлення.
- Створити ярлик
- Ніколи не показувати
- Рекомендується менеджер-паразит
- LSPosed тепер підтримує паразитування системи, щоб уникнути виявлення, ви можете відкрити диспетчер паразитів з повідомлення. Рекомендується видалити поточну програму.
-
- Зберегти
- Детальні звіти
- Звіти модулів
- Збереження журналу, зачекайте
- Журнали збережено
- Не вдалося зберегти:\n%s
- Очистити журнал зараз
- Журнал успішно очищено.
- Прокрутити догори
- Завантаження…
- Прокрутити донизу
- Перезавантажити
- Не вдалося очистити журнал
- Перенесення слів
- Детальний журнал увімкнено
- Детальний журнал вимкнено
-
- (опису не надано)
- Цей модуль вимагає новішої версії Xposed (%d), тому його неможливо застосувати
- Цей модуль розроблено для новішої версії Xposed (%d), тому деякі функції можуть не працювати
- У цьому модулі не вказано необхідну для нього версію Xposed.
- Цей модуль був створений для Xposed версії %1$d, але через несумісні зміни у версії %2$d його було вимкнено
- Цей модуль не може бути завантажений, оскільки він встановлений на SD-карті. Будь ласка, перенесіть його у внутрішню пам’ять
- Видалити
- Налаштування модуля
- Переглянути в репозиторії
- Ви впевнені, що хочете видалити цей модуль?
- Видалено %1$s
- Видалення не виконано
- Додати модуль до користувача
- Додано %1$s до користувача %2$s
- Не вдалося додати модуль
- Встановити користувачу %s
- Бажаєте встановити %1$s користувачу %2$s? Рекомендується встановлювати вручну. Примусове встановлення за допомогою LSPosed може спричинити проблеми.
- розгорнути
- згорнути
-
- Повторна оптимізація
- Оптимізація…
- Оптимізація завершена
- Запустити
- Помилка оптимізації: повернуте значення порожнє
- Помилка оптимізації:
- За назвою програми
- За іменем пакету
- За часом встановлення
- За часом оновлення
- У зворотньому порядку
- Системні програми
- Сортування
- Увімкнути модуль
- Ви не вибрали жодної програми. Продовжити?
- Ігри
- Модулі
- Не вдалося зберегти список областей
- Версія: %1$s
- Виберіть
- Рекомендовано
- Ви не вибрали жодної програми. Вибрати рекомендовані програми?
- Вибрати рекомендовані програми?
- Усі
- Немає
- Автовключення
- Модуль LSPosed ще не активований
- Рекомендовано
- Доступне оновлення: %1$s
- Модуль %s вимкнено, оскільки жодна програма не вибрана.
- System Framework
- Резервне копіювання
- Створити резервну копію
- Відновити з резервної копії
- Примусово зупинити
- Примусово зупинити?
- Примусове зупинення додатку, може мати наслідки.
- Для застосування цієї зміни потрібне перезавантаження
- Перезавантажити
- Приховати
-
- Переглянути в іншому додатку
- Про програму
- ¯\\\\_(ツ)_\/¯\nТут нічого немає
-
- Framework
- Вимкнути детальні журнали
- Вимоги до звітів про проблеми, що виникають, включати розгорнуті журнали
- Чорна тема
- Використовувати повністю чорну тему, коли темну тему увімкнено
- Тема
- Резервне копіювання та відновлення
- Зберегти список увімкнених модулів та їхніх програм-жертв
- Відновити список увімкнених модулів та їхніх програм-жертв
- Створити резервну копію
- Не вдалося створити резервну копію:\n%s
- Будь ласка, увімкніть DocumentUI
- Відновити з резервної копії
- Не вдалося зберегти:\n%s
- Мережа
- DNS поверх HTTPS
- Обійти регіональне DNS-блокування
- Колір акценту
- Системний колір акценту
- Примусово показувати значки запуску
- Починаючи з Android 10, програмам не дозволяється приховувати свої значки запуску. Вимкніть перемикач, щоб вимкнути цю системну функцію.
- Система
- Мова
- Автори перекладу
- Допомогти з перекладом
- Доповнити український переклад %s
- Створіть ярлик, який зможе відкрити паразитний менеджер
- Ярлик закріплено
- Поточна панель запуску за замовчуванням не підтримує шорткати пін-кодів
- Повідомлення про статус
- Показати повідомлення, яке може відкрити паразитний менеджер
- Канал оновлень
- Стабільний
- Бета-версія
- Нічна збірка
-
- Опис
- Релізи
- Інформація
- Домашня сторінка
- Вихідний код
- Співавтори
- Вкладення
- Відкрити в браузері
- Показувати старіші версії
- Більше релізів немає
- Не вдалося завантажити модуль репозиторія: %s
- Спершу нові версії
- встановлено
-
- - %d завантажити
- - %d завантажень
- - %d завантажень
- - %d завантажень
-
-
- Вишневий
- Червоний
- Рожевий
- Фіолетовий
- Темно-фіолетовий
- Індиго
- Синій
- Світло-блакитний
- Бірюзовий
- Зеленувато-блакитний
- Зелений
- Світло-зелений
- Лаймовий
- Жовтий
- Бурштиновий
- Помаранчевий
- Жовтогарячий
- Коричневий
- Блакитно-сірий
-
diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml
deleted file mode 100644
index aef2ee739..000000000
--- a/app/src/main/res/values-ur/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- جائزہ
- ماڈیولز
-
- - %d ماڈیولز آن ہیں۔
- - %d ماڈیولز فعال ہیں۔
-
- نوشتہ جات
- ترتیبات
- رائے یا تجویز
- معلومات
- رپورٹ ایشو۔
- مخزن۔
- تمام ماڈیولز اپ ٹو ڈیٹ
- %sپر شائع ہوا۔
- %sپر اپ ڈیٹ کیا گیا۔
-
- - %d ماڈیول قابل اپ گریڈ
- - %d ماڈیولز قابل اپ گریڈ
-
- ہمارے ساتھ شامل ہوں%2$s چینل۔]]>
- PGH Gaming Official
- انسٹال
- کلک تو انسٹال LSposed
- یہ انسٹال نہیں ہے۔
- LSposed انسٹال نہیں ہے۔
- یہ چالو ہے۔
- جزوی طور پر فعال
- SePolicy ٹھیک سے آن نہیں ہے
-
- سسٹم فریم ورک انجکشن ناکام۔
- Magisk یا کچھ کم معیار کے Magisk ماڈیولز کی وجہ سے ہو سکتا ہے۔ براہ کرم Riru اور LSPosed کے علاوہ Magisk ماڈیولز کو غیر فعال کرنے کی کوشش کریں یا مکمل لاگ ڈویلپرز کو جمع کرائیں۔]]>
- سسٹم پروپ غلط ہے۔
- ماڈیول کبھی کبھار باطل ہو سکتے ہیں۔]]>
- اپ ڈیٹ درکار ہے۔
- براہ کرم LSPosed کا تازہ ترین ورژن انسٹال کریں۔
- API ورژن۔
- فریم ورک ورژن۔
- مینیجر پیکیج کا نام
- سسٹم ورژن۔
- ڈیوائس۔
- ABI سسٹم۔
- ڈیکس آپٹیمائزر ریپر
- فعال
- فعال نہیں ہے۔
- حمایت یافتہ
- غیر تعاون یافتہ
- Android ورژن غیر مطمئن ہے۔
- کریش ہو گیا۔
- ماؤنٹ ناکام ہوگیا۔
- SELinux کی اجازت ہے۔
- SELinux پالیسی غلط ہے۔
- LSPosed کو اپ ڈیٹ کریں۔
- LSPosed کو اپ ڈیٹ کرنے کی تصدیق کریں؟ یہ آلہ اپ ڈیٹ مکمل ہونے کے بعد دوبارہ شروع ہو جائے گا۔
- کلپ بورڈ پر کاپی ہو گیا۔
-
- LSPosed میں خوش آمدید
- آپ پرجیوی مینیجر کا استعمال کر رہے ہیں، جو شارٹ کٹ بنا سکتا ہے یا پھر بھی اطلاع سے کھل سکتا ہے۔
- آپ پرجیوی مینیجر استعمال کر رہے ہیں، جو اطلاع سے کھل سکتا ہے۔
- شارٹ کٹ بنانا
- کبھی نہ دکھائیں۔
- پرجیوی مینیجر کی سفارش
- LSPosed اب پتہ لگانے سے بچنے کے لیے سسٹم پرجیوی کی حمایت کرتا ہے، آپ نوٹیفکیشن سے پرجیوی مینیجر کو کھول سکتے ہیں۔ موجودہ ایپلیکیشن کو ان انسٹال کرنے کی سفارش کی جاتی ہے۔
-
- محفوظ کریں
- وربوز لاگز
- ماڈیولز نوشتہ جات
- لاگ محفوظ کر رہا ہے، براہ کرم انتظار کریں۔
- لاگز محفوظ ہو گئے۔
- محفوظ کرنے میں ناکام:\n%s
- ابھی لاگ صاف کریں۔
- لاگ کامیابی سے صاف ہو گیا.
- اوپر سکرول کریں۔
- لوڈ ہو رہا ہے…
- نیچے سکرول کریں۔
- دوبارہ لوڈ کریں۔
- لاگ کو صاف کرنے میں ناکام
- لفظ لفاف
- وربوز لاگ فعال ہے۔
- وربوز لاگ غیر فعال ہے۔
-
- (کوئی تفصیل فراہم نہیں کی گئی)
- اس ماڈیول کے لیے ایک نئے Xposed ورژن (%d) کی ضرورت ہے اور اس طرح اسے چالو نہیں کیا جا سکتا
- یہ ماڈیول ایک نئے Xposed ورژن (%d) کے لیے ڈیزائن کیا گیا ہے اور اس طرح کچھ افعال کام نہیں کر سکتے
- یہ ماڈیول اس Xposed ورژن کی وضاحت نہیں کرتا جس کی اسے ضرورت ہے۔
- یہ ماڈیول Xposed ورژن %1$dکے لیے بنایا گیا تھا، لیکن ورژن %2$dمیں غیر موافق تبدیلیوں کی وجہ سے، اسے غیر فعال کر دیا گیا ہے۔
- اس ماڈیول کو لوڈ نہیں کیا جا سکتا کیونکہ یہ SD کارڈ پر انسٹال ہے، براہ کرم اسے اندرونی اسٹوریج میں منتقل کریں۔
- ان انسٹال کریں۔
- ماڈیول کی ترتیبات
- ریپو میں دیکھیں
- کیا آپ اس ماڈیول کو ان انسٹال کرنا چاہتے ہیں؟
- ان انسٹال %1$s
- اَن انسٹال ناکام
- صارف میں ماڈیول شامل کریں۔
- صارف %2$sمیں %1$s شامل کیا گیا۔
- ماڈیول شامل کرنا ناکام ہو گیا۔
- صارف %sپر انسٹال کریں۔
- صارف %2$sپر %1$s انسٹال کرنا چاہتے ہیں؟ یہ دستی طور پر انسٹال کرنے کی سفارش کی جاتی ہے، LSPosed کے ذریعے انسٹال کرنے سے مسائل پیدا ہو سکتے ہیں۔
- پھیلاؤ
- گرنے
-
- دوبارہ اصلاح کریں۔
- …کو بہتر بنانا
- اصلاح مکمل
- اسے لانچ کریں۔
- اصلاح ناکام: واپسی کی قدر خالی ہے۔
- اصلاح ناکام:
- درخواست کا نام
- پیکیج کا نام
- انسٹال کرنے کا وقت
- اپ ڈیٹ کا وقت
- معکوس
- سسٹم ایپس
- چھانٹنا
- ماڈیول کو فعال کریں۔
- آپ نے کوئی ایپ منتخب نہیں کی۔ جاری رہے؟
- کھیل
- ماڈیولز
- دائرہ کار کی فہرست کو محفوظ کرنے میں ناکام
- ورژن: %1$s
- سفارش کریں۔
- آپ نے کوئی ایپ منتخب نہیں کی۔ تجویز کردہ ایپس کو منتخب کریں؟
- تجویز کردہ ایپس کو منتخب کریں?
- Xposed ماڈیول ابھی تک چالو نہیں ہوا ہے۔
- سفارش کریں۔
- اپ ڈیٹ دستیاب ہے: %1$s
- ماڈیول%s کو غیر فعال کر دیا گیا ہے کیونکہ کوئی ایپ منتخب نہیں کی گئی ہے.
- سسٹم فریم ورک
- بیک اپ
- بیک اپ
- بحال کریں۔
- زبردستی روکنا
- زبردستی روکنا؟
- اگر آپ کسی ایپ کو زبردستی روکتے ہیں تو یہ غلط برتاؤ کر سکتی ہے۔
- اس تبدیلی کو لاگو کرنے کے لیے ریبوٹ کی ضرورت ہے۔
- دوبارہ شروع کریں۔
- چھپائیں
-
- دوسری ایپ میں دیکھیں
- ایپ کی معلومات
- ¯\\\\_(ツ)_\/¯\nیہاں کچھ نہیں ہے۔
-
- فریم ورک
- وربوز لاگز کو غیر فعال کریں۔
- وربوز لاگز کو شامل کرنے کے لیے مسائل کی اطلاع دیں۔
- سیاہ سیاہ تھیم
- اگر سیاہ تھیم فعال ہو تو خالص سیاہ تھیم استعمال کریں۔
- خیالیہ
- بیک اپ اور بحال
- بیک اپ ماڈیول کی فہرست اور دائرہ کار کی فہرستیں۔
- ماڈیول کی فہرست اور دائرہ کار کی فہرستوں کو بحال کریں۔
- بیک اپ
- بیک اپ کرنے میں ناکام:\n%s
- براہ کرم DocumentUI کو فعال کریں۔
- بحال کریں۔
- بحال کرنے میں ناکام:\n%s
- نیٹ ورک
- HTTPS پر DNS
- کچھ قوموں میں DNS زہر کا حل
- تھیم کا رنگ
- سسٹم تھیم کا رنگ
- ایپس کو لانچر آئیکنز دکھانے پر مجبور کریں۔
- اینڈرائیڈ 10 کے بعد ایپس کو اپنے لانچر آئیکنز کو چھپانے کی اجازت نہیں ہے۔ اس سسٹم کی خصوصیت کو غیر فعال کرنے کے لیے ٹوگل کو بند کریں۔
- سسٹم
- زبان
- ترجمہ کرنے والے
- ترجمہ میں حصہ لیں۔
- %s کا اپنی زبان میں ترجمہ کرنے میں ہماری مدد کریں۔
- ایک شارٹ کٹ بنائیں جو پرجیوی مینیجر کو کھول سکے۔
- شارٹ کٹ پن کر دیا گیا۔
- موجودہ ڈیفالٹ لانچر پن شارٹ کٹس کو سپورٹ نہیں کرتا ہے۔
- اسٹیٹس کی اطلاع
- ایک اطلاع دکھائیں جو پرجیوی مینیجر کو کھول سکے۔
- چینل کو اپ ڈیٹ کریں۔
- مستحکم
- بیٹا
- رات کی تعمیر
-
- ریڈمی
- جاری کرتا ہے۔
- معلومات
- ہوم پیج
- سورس کوڈ
- تعاون کرنے والے
- اثاثے
- براؤزر میں کھولیں
- پرانے ورژن دکھائیں۔
- مزید رہائی نہیں ہے۔
- ماڈیول ریپو لوڈ کرنے میں ناکام: %s
- پہلے اپ گریڈ کیا جا سکتا ہے۔
- انسٹال
-
- - %d ڈاؤن لوڈ
- - %d ڈاؤن لوڈز
-
-
- ساکورا
- سرخ
- گلابی
- جامنی
- گہرے جامنی رنگ
- انڈگو
- نیلا
- ہلکے نیلے رنگ کے
- سیان
- ٹیل
- سبز
- ہلکا سبز
- لیموں
- پیلا
- امبر
- کینو
- گہرا نارنجی
- براؤن
- بلیو گرے
-
diff --git a/app/src/main/res/values-v28/dimens.xml b/app/src/main/res/values-v28/dimens.xml
deleted file mode 100644
index ecf7876d5..000000000
--- a/app/src/main/res/values-v28/dimens.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- 12dp
-
diff --git a/app/src/main/res/values-v28/themes.xml b/app/src/main/res/values-v28/themes.xml
deleted file mode 100644
index be5c2c91f..000000000
--- a/app/src/main/res/values-v28/themes.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-v29/settings.xml b/app/src/main/res/values-v29/settings.xml
deleted file mode 100644
index ebb9cc6ee..000000000
--- a/app/src/main/res/values-v29/settings.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- true
-
diff --git a/app/src/main/res/values-v30/themes.xml b/app/src/main/res/values-v30/themes.xml
deleted file mode 100644
index b182fe6bb..000000000
--- a/app/src/main/res/values-v30/themes.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-v31/colors.xml b/app/src/main/res/values-v31/colors.xml
deleted file mode 100644
index 7da0b37bf..000000000
--- a/app/src/main/res/values-v31/colors.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- @android:color/system_accent1_0
- @android:color/system_accent1_600
-
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
deleted file mode 100644
index 0065b198d..000000000
--- a/app/src/main/res/values-vi/strings.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
- Tổng quan
- Mô-đun
-
- - %d Mô-đun đã bật
-
- Nhật ký
- Cài đặt
- Phản hồi hoặc góp ý
- Giới thiệu
- Báo cáo sự cố
- Kho
- Tất cả các modules đã được cập nhật
- Xuất bản lúc %s
- Cập nhật lúc %s
-
- - %d Modules có bản cập nhật
-
- Tham gia kênh %2$s của chúng tôi]]>
- The Primal Pea, Tuyen Nguyen (tuyennn)
- Cài đặt
- Ấn vào đây để cài đặt LSPosed
- Chưa được cài đặt
- LSPosed chưa được cài đặt
- Đã được kích hoạt
- Một phần đã được kích hoạt
- Chính sách SELinux không được nạp đúng cách
- Vui lòng báo cáo đến nhà phát triển .]]>
- Không thể nhúng vào khung hệ thống
- Magisk hoặc 1 số mô-đun Magisk kém chất lượng. Hãy thử vô hiệu hóa những mô-đun Magisk đó, thử chạy riêng Riru và LSPosed mà thôi hoặc thông báo nhật ký tới những nhà phát triển.]]>
- Thông tin hệ thống không đúng
- Đôi khi, các mô-đun có thể sẽ mất hiệu lực.]]>
- Cần được cập nhật
- Vui lòng cài đặt phiên bản mới nhất của LSPosed
- Mẹo cho module developer
- Vui lòng tắt tối ưu hóa triển khai trên Android Studio hoặc sử dụng lệnh `gradlew installDebug` để cài đặt. Nếu không, apk mô-đun sẽ không được cập nhật.
- Phiên bản API
- Phiên bản Framework
- Tên gói quản lý
- Phiên bản hệ thống
- Thiết bị
- Hệ thống ABI
- Trình tối ưu Dex Wrapper
- Đã bật
- Chưa bật
- Được hỗ trợ
- Không được hỗ trợ
- Phiên bản Android không hỗ trợ
- Bị lỗi
- Mount thất bại
- Cho phép SELinux
- Chính sách SELinux không chính xác
- Cập nhật LSPosed
- Xác nhận cập nhật LSPosed? Thiết bị này sẽ khởi động lại sau khi hoàn tất cập nhật
- Đã sao chép vào bảng nhớ tạm
-
- Chào mừng
- Đang sử dụng trình quản lý phụ thuộc có thể tạo lối tắt hoặc mở từ thông báo.
- Đang sử dụng trình quản lý phụ thuộc có thể mở từ thông báo.
- Tạo lối tắt
- Không hiện nữa
- Quản lý phụ thuộc khuyến nghị
- Ứng dụng hiện hỗ trợ phụ thuộc hệ thống để tránh bị phát hiện có thể mở trình quản lý từ thông báo.
-Nên gỡ cài đặt ứng dụng hiện tại.
-
- Lưu lại
- Nhật ký Chi tiết
- Nhật ký các Mô-đun
- Đang lưu nhật ký, vui lòng đợi
- Nhật ký đã được lưu
- Lưu thất bại:\n%s
- Xóa nhật ký
- Nhật ký đã được xoá.
- Cuộn lên trên
- Đang tải…
- Cuộn xuống dưới
- Tải lại
- Xoá nhật kí thất bại
- Tự động xuống dòng
- Nhật ký chi tiết đã được kích hoạt
- Nhật ký chi tiết đã được vô hiệu hoá
-
- (chưa có mô tả)
- Mô-đun này yêu cầu một phiên bản Xposed mới hơn (%d) và đó là lý do không thể được kích hoạt
- Tiện ích bổ sung này được làm cho phiên bản ứng dụng mới hơn (%d) nên một số chức năng có thể không hoạt động
- Mô-đun này không chỉ định phiên bản Xposed cần thiết để khởi chạy.
- Mô-đun này được tạo bởi phiên bản Xposed %1$d, nhưng vì lý do không tương thích với phiên bản %2$d, nên nó đã bị vô hiệu hóa
- Mô-đun này không được nạp vì nó được cài đặt trên thẻ nhớ SD, vui lòng chuyển nó vào bộ nhớ trong
- Gỡ cài đặt
- Cài đặt Mô-đun
- Xem ở trên Kho
- Bạn có muốn gỡ cài đặt mô-đun này?
- Đã gỡ cài đặt %1$s
- Gỡ cài đặt không thành công
- Thêm mô-đun tới người dùng
- Đã thêm %1$s tới người dùng %2$s
- Thêm mô-đun thất bại
- Cài đặt tới người dùng %s
- Muốn cài %1$s tới người dùng %2$s? Khuyến cáo bạn nên cài đặt thủ công, buộc cài đặt qua LSPosed có thể xảy ra vấn đề không mong muốn.
- mở
- đóng
-
- Tối ưu lại
- Đang tối ưu…
- Tối ưu hoàn tất
- Khởi chạy
- Tối ưu thất bại: trả về giá trị trống
- Tôi ưu thất bại:
- Tên ứng dụng
- Tên gói ứng dụng
- Thời gian cài đặt
- Thời gian cập nhật
- Đảo ngược
- Ứng dụng hệ thống
- Sắp xếp
- Kích hoạt mô-đun
- Bạn đã không lựa chọn bất kỳ ứng dụng nào. Tiếp tục chứ?
- Trò chơi
- Mô-đun
- Lưu danh sách phạm vi thất bại
- Phiên bản: %1$s
- Được khuyến cáo
- Bạn đã không lựa chọn bất kỳ ứng dụng nào. Lựa chọn những ứng dụng được khuyến nghị?
- Lựa chọn những ứng dụng được khuyến cáo?
- Mô-đun Xposed chưa được kích hoạt
- Được khuyến cáo
- Cập nhật khả dụng: %1$s
- Mô-đun %s đã bị vô hiệu hoá do không có ứng dụng nào được lựa chọn.
- Framework Hệ thống
- Sao lưu
- Sao lưu
- Phục hồi
- Buộc dừng
- Buộc dừng?
- Nếu bạn buộc dừng một ứng dụng, nó có thể gặp lỗi.
- Khởi động lại là bắt buộc cho thay đổi này
- Khởi động lại
- Ẩn
-
- Xem trong ứng dụng khác
- Thông tin ứng dụng
- ¯\\\\_(ツ)_\/¯\nKhông có gì ở đây cả
-
- Khung hệ thống
- Vô hiệu hoá nhật ký chi tiết
- Báo cáo sự cố yêu cầu bao gồm nhật ký chi tiết
- Chủ đề Đen - Tối
- Sử dụng chủ đề đen nếu chủ đề tối được bật
- Chủ đề
- Sao lưu và khôi phục
- Sao lưu danh sách mô-đun và danh sách phạm vi.
- Khôi phục danh sách mô-đun và danh sách phạm vi.
- Sao lưu
- Sao lưu thất bại:\n%s
- Vui lòng kích hoạt DocumentUI
- Phục hồi
- Phục hồi thất bại:\n%s
- Mạng
- DNS qua HTTPS
- Giải pháp khắc phục tình trạng giả mạo DNS ở một số quốc gia
- Màu chủ đề
- Màu chủ đề hệ thống
- Buộc các ứng dụng hiển thị biểu tượng trên trình khởi chạy
- Sau Android 10, các ứng dụng không được phép ẩn biểu tượng trên trình khởi chạy. Tắt lựa chọn này để vô hiệu tính năng này của hệ thống.
- Hệ thống
- Ngôn ngữ
- Cộng tác viên phiên dịch
- Tham gia phiên dịch
- Giúp chúng tôi dịch %s sang ngôn ngữ của bạn
- Tạo lối tắt để mở trình quản lý phụ thuộc
- Đã ghim phím tắt
- Trình khởi chạy hiện tại không hỗ trợ tạo lối tắt
- Thông báo trạng thái
- Hiện thông báo để mở trình quản lý phụ thuộc
- Kênh cập nhật
- Ổn định
- Thử nghiệm
- Bản dựng hàng đêm
-
- Đọc
- Bản phát hành
- Thông tin
- Trang chủ
- Mã nguồn
- Cộng tác viên
- Tài sản
- Mở trong trình duyệt
- Hiển thị các phiên bản cũ hơn
- Không có phiên bản nào
- Tải kho lưu trữ mô-đun thất bại: %s
- Có thể nâng cấp trước
- Đã được cài đặt
-
- - %d lượt tải xuống
-
-
- Hoa anh đào
- Đỏ
- Hồng
- Tím
- Tím đậm
- Xanh đậm
- Xanh
- Xanh sáng
- Lục lam
- Mòng két
- Xanh lá
- Xanh lá sáng
- Xanh chanh
- Vàng
- Hổ phách
- Cam
- Cam đậm
- Nâu
- Xanh xám
-
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
deleted file mode 100644
index 2f3fc89c9..000000000
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ /dev/null
@@ -1,240 +0,0 @@
-
-
-
-
- 概览
- 模块
-
- - 已启用 %d 个模块
-
- 日志
- 设置
- 反馈或建议
- 关于
- 反馈问题
- 仓库
- 所有模块均已最新
- 发布于 %s
- 更新于 %s
-
- - %d 个模块可更新
-
- 加入我们的 %2$s 频道 加入我们的 QQ 频道 ]]>
- LSPosed
-JingMatrix
- 安装
- 点击安装 LSPosed
- 未安装
- LSPosed 未安装
- 已激活
- 部分激活
- SEPolicy 未被正确加载
- 请将此问题报告给 Magisk 开发者]]>
- 系统框架注入失败
- Magisk 或低质 Magisk 模块导致。 请尝试禁用除 Riru 和 LSPosed 外的其他 Magisk 模块,或向开发者提供完整日志。]]>
- 系统属性异常
- 模块可能会随机失效。]]>
- 需要更新
- 请安装新版 LSPosed
- 给模块开发者的提示
- 请在 Android Studio 上禁用部署优化,或使用 `gradlew installDebug` 命令进行安装,否则无法更新模块。
- API 版本
- 框架版本
- 管理器包名
- 系统版本
- 设备
- 系统架构
- Dex 优化器包装
- 已启用
- 未启用
- 支持
- 不支持
- 系统版本不受支持
- 崩溃
- 挂载失败
- SELinux 处于宽容模式
- SELinux 规则异常
- 更新 LSPosed
- 确认更新 LSPosed? 设备将会在完成更新后自动重启。
- 已复制到剪贴板
-
- 欢迎使用 LSPosed
- 你正在使用寄生管理器,可创建快捷方式或继续从通知中打开。
- 你正在使用寄生管理器,可以从通知打开它。
- 创建快捷方式
- 不再显示
- 推荐使用寄生管理器
- LSPosed 现在支持系统寄生以避免检测,你可以从通知中打开寄生管理器。建议卸载当前应用。
-
- 保存
- 详细日志
- 模块日志
- 正在保存日志,请稍后
- 日志已保存
- 保存失败:\n%s
- 立即清空日志
- 成功清空日志。
- 滚动到顶部
- 加载中…
- 滚动到底部
- 重新加载
- 日志清空失败
- 自动换行
- 详细日志已启用
- 详细日志已禁用
-
- (未提供介绍)
- 此模块需要更新的 Xposed 版本(%d),因此无法激活
- 此模块是为较新的 Xposed 版本(%d)设计的,因此某些功能可能无法使用
- 该模块未指定所需的 Xposed 版本
- 由于该模块开发时所基于 Xposed %1$d 版本不再兼容 %2$d 版本中的变更,该模块现已被停用
- 此模块因被安装在 SD 卡中而导致无法加载,请将其移动到内部存储
- 卸载
- 模块设置
- 在仓库中查看
- 确认卸载该模块?
- 已卸载 %1$s
- 卸载失败
- 安装模块到用户
- 已安装 %1$s 到用户 %2$s
- 安装失败
- 安装到用户 %s
- 确认安装 %1$s 到用户 %2$s?推荐手动用系统自带方法安装,通过 LSPosed 强制安装可能会导致未知异常。
- 展开
- 收起
-
- 重新优化
- 优化中…
- 优化完成
- 启动
- 优化失败:返回值为空
- 优化失败:
- 应用名
- 包名
- 安装时间
- 更新时间
- 倒序
- 系统应用
- 排序
- 启用模块
- 未选择任何应用。继续?
- 游戏
- 模块
- 作用域列表保存失败
- 版本:%1$s
- 选择
- 勾选推荐
- 未选择任何应用。选择推荐的应用?
- 选择推荐的应用?
- 全部
- 空
- 自动添加
- Xposed 模块尚未激活
- 推荐应用
- 可用更新:%1$s
- 由于未选择任何应用,模块 %s 已被禁用。
- 系统框架
- 备份
- 备份
- 恢复
- 强行停止
- 要强行停止吗?
- 强行停止某个应用可能会使其异常。
- 重启以应用此更改
- 重启系统
- 隐藏
-
- 在其它应用中查看
- 应用信息
- ¯\\\\_(ツ)_\/¯\n空空如也
-
- 框架
- 禁用详细日志
- 报告问题要求包含详细日志
- 纯黑主题
- 当深色主题启用时使用纯黑主题
- 主题
- 备份与恢复
- 备份模块列表与作用域列表
- 恢复模块列表与作用域列表
- 备份
- 备份失败:\n%s
- 请启用文档应用
- 恢复
- 恢复失败:\n%s
- 网络
- 安全 DNS(DoH)
- 解决某些地区的 DNS 污染问题
- 主题颜色
- 系统主题色
- 强制显示桌面图标
- 在 Android 10 或更高版本,应用不再允许隐藏桌面图标。关闭该选项以关闭该系统功能。
- 系统
- 语言
- 译者
- 参与翻译
- 帮助我们把 %s 翻译到你的语言
- 创建一个能打开寄生管理器的快捷方式
- 已创建快捷方式
- 当前默认桌面不支持固定快捷方式
- 状态通知
- 显示一个通知以打开寄生管理器
- 模块更新通道
- 稳定版
- 测试版
- 每夜版
-
- 自述文件
- 版本
- 信息
- 主页
- 源码
- 协作者
- 附件
- 在浏览器中打开
- 显示较早版本
- 无更多版本
- 模块仓库加载失败:%s
- 可更新优先
- 已安装
-
- - %d 次下载
-
-
- 樱花
- 红色
- 粉色
- 紫色
- 深紫
- 靛青
- 蓝色
- 浅蓝
- 青色
- 青绿
- 绿色
- 浅绿
- 黄绿
- 黄色
- 琥珀
- 橙色
- 深橙
- 棕色
- 灰蓝
-
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
deleted file mode 100644
index 3e2ed8526..000000000
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ /dev/null
@@ -1,239 +0,0 @@
-
-
-
-
- 概觀
- 模組
-
- - %d 個模組已啟用
-
- 記錄
- 設定
- 回饋或建議
- 關於
- 回報問題
- 資料庫
- 所有模組為最新版本
- 發佈於 %s
- 更新於 %s
-
- - %d 個模組可更新
-
- 加入我們的 %2$s 頻道]]>
- DarKnighT0v0
- 安裝
- 點選安裝 LSPosed
- 未安裝
- LSPosed 未安裝
- 已啟用
- 部分啟用
- SEPolicy 未被正確讀取
- 請將此回報給 Magisk 開發人員。]]>
- 系統架構插入失敗
- Magisk 或低品質 Magisk 模組導致, 請嘗試停用除 Riru 和 LSPosed 外的 Magisk 模組,或向開發人員提供完整記錄。]]>
- 系統屬性異常
- 模組可能會隨機失效。]]>
- 需要更新
- 請安裝最新版本的 LSPosed
- 給模組開發人員的提示
- 請在 Android Studio 上停用部署最佳化,或使用 `gradlew installDebug` 指令進行安裝。否則模組apk將不會更新。
- API版本
- 架構版本
- 管理器包名
- 系統版本
- 裝置版本
- 系統架構
- Dex 最佳化包裝函式
- 已啟用
- 未啟用
- 支援
- 不支援
- 系統版本不受支援
- 崩潰
- 加載失敗
- SELinux 處於寬容模式
- SELinux 原則異常
- 更新 LSPosed
- 確認更新LSPosed? 此裝置會於更新完成後重啟
- 已複製到剪貼簿
-
- 歡迎使用 LSPosed
- 您正在使用寄生管理員,您可以建立捷徑或從通知開啟。
- 您正在使用寄生管理員,它可以從通知中開啟。
- 建立捷徑
- 不再顯示
- 建議使用寄生管理員
- LSPosed 現在支援系統寄生以避免偵測,您可以從通知開啟寄生管理員。建議解除安裝目前應用程式。
-
- 儲存
- 詳細記錄
- 模組記錄
- 正在保存日誌,請稍候
- 記錄已儲存
- 儲存失敗:\n%s
- 立即清理記錄
- 記錄清理成功
- 移至頂端
- 正在載入…
- 移至底端
- 重新載入
- 記錄清理失敗
- 自動換行
- 詳細記錄已啟用
- 詳細記錄已停用
-
- (未提供描述)
- 此模組需要更新版本的 Xposed 版本 (%d),因此無法被啟用
- 此模組專為較新的 Xposed 版本 (%d) 而設計,因此某些功能可能無法運作
- 該模組未指定需要的 Xposed 版本
- 此模組適用於 %1$d 版本的 Xposed ,由於版本 %2$d 的變更不相容,因此已經停用此模組
- 由於此模組被安裝在SD卡中而無法載入,請將其移動到內部儲存空間
- 解除安裝
- 模組設定
- 在存放庫中檢視
- 您確定要移除此模組嗎?
- 已移除 %1$s
- 移除失敗
- 為用戶安裝模組
- 已為用戶 %2$s 安裝模組 %1$s
- 模組安裝失敗
- 為用戶 %s 安裝
- 確定要為用戶 %2$s 安裝 %1$s 嗎?建議手動安裝或多開,透過 LSPosed 強制安裝可能會出現問題。
- 展開
- 收起
-
- 重新最佳化
- 正在最佳化…
- 最佳化完成
- 執行
- 最佳化失敗或返回值為空
- 最佳化失敗:
- 應用程式名稱
- 套件名稱
- 安裝時間
- 更新時間
- 遞減
- 系統應用程式
- 排序
- 啟用模組
- 未選擇任何應用程式,是否繼續?
- 遊戲
- 模組
- 作用範圍清單儲存失敗
- 版本:%1$s
- 選擇
- 推薦應用程式
- 未選擇任何應用程式,選擇推薦的應用程式?
- 選擇推薦的應用程式?
- 全部
- 空
- 自動添加
- Xposed 模組尚未啟用
- 推薦應用程式
- 可用更新:%1$s
- 由於未選擇任何應用程式,模組 %s 已被停用。
- 系統架構
- 備份
- 備份
- 還原
- 強制停止
- 確定要強制停止?
- 如果您強制停止應用程式,可能會導致行為異常。
- 重啟以套用變更
- 重啟系統
- 隱藏
-
- 在其他應用程式中檢視
- 應用程式資訊
- ¯\\\\_(ツ)_\/¯\n這裡甚麼都沒有
-
- 框架
- 禁用詳細紀錄檔
- 回報問題要求包含詳細記錄檔
- 使用純黑深色主題
- 使用純黑色背景當深色模式已啟用
- 主題
- 備份與還原
- 備份模組清單和作用範圍清單
- 還原模組清單和作用範圍清單
- 備份
- 備份失敗:\n%s
- 請啟用 DocumentUI
- 還原
- 還原失敗:\n%s
- 網絡
- 安全 DNS(DoH)
- 解決部分地區的 DNS 中毒問題
- 主題色彩
- 系統主題色彩
- 強制應用程式在啟動器中顯示圖示
- Android 10之後不允許隱藏桌面圖示。關閉開關以禁用此功能
- 系統
- 語言
- 譯者
- 參與翻譯
- 幫助我們翻譯 %s 到您的語言
- 建立可以開啟寄生管理員的捷徑
- 捷徑已釘選
- 目前的預設啟動器不支援釘選捷徑
- 狀態通知
- 顯示通知以便開啟寄生管理員
- 更新頻道
- 穩定版
- 測試版
- 每夜構建
-
- 自述文件
- 版本
- 資訊
- 首頁
- 原始程式碼
- 合作者
- 附件
- 在瀏覽器中打開
- 顯示較舊版本
- 沒有更舊版本
- 模組存放庫載入失敗:%s
- 可更新優先
- 已安裝
-
- - %d 次下載
-
-
- 櫻花
- 紅色
- 粉色
- 紫色
- 深紫
- 靛青
- 藍色
- 淺藍
- 青色
- 青綠
- 綠色
- 淺綠
- 萊姆綠
- 黃色
- 琥珀
- 橙色
- 深橙
- 棕色
- 藍灰
-
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
deleted file mode 100644
index 82de0478d..000000000
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ /dev/null
@@ -1,235 +0,0 @@
-
-
-
-
- 概觀
- 模組
-
- - %d 個模組已啟用
-
- 日誌
- 設定
- 回饋或建議
- 關於
- 回報問題
- 倉庫
- 所有模組為最新版本
- 發佈於 %s
- 更新於 %s
-
- - %d 個模組可更新
-
- 加入我們的 %2$s 頻道]]>
- 孟武. 尼德霍格. 龍、david082321、beigua87
- 安裝
- 點選安裝 LSPosed
- 未安裝
- LSPosed 未安裝
- 已啟用
- 部分啟用
- SEPolicy 未被正確讀取
- 請將此回報給 Magisk 開發人員。]]>
- 系統框架注入失敗
- Magisk 或低品質 Magisk 模組導致, 請嘗試停用除 Riru 和 LSPosed 外的 Magisk 模組,或向開發者提供完整日誌。]]>
- 系統屬性異常
- 模組可能會隨機失效。]]>
- 需要更新
- 請安裝最新版本的 LSPosed
- 給模組開發人員的提示
- 請在 Android Studio 上停用部署最佳化,或使用 `gradlew installDebug` 指令進行安裝。否則模組apk將不會更新。
- API 版本
- 框架版本
- 管理器包名
- 系統版本
- 裝置版本
- 系統架構
- Dex 優化器包裝器
- 已啟用
- 未啟用
- 支援
- 不支援
- 系統版本不受支援
- 當機
- 掛載失敗
- SELinux 處於寬容模式
- SELinux 規則異常
- 更新 LSPosed
- 確定要更新LSPosed嗎?更新完成後將會重啟裝置
- 已複製到剪貼簿
-
- 歡迎使用 LSPosed
- 您正在使用寄生管理員,您可以建立捷徑或從通知開啟。
- 您正在使用寄生管理員,它可以從通知中開啟。
- 建立捷徑
- 不再顯示
- 建議使用寄生管理員
- LSPosed 現在支援系統寄生以避免偵測,您可以從通知開啟寄生管理員。建議解除安裝目前應用程式。
-
- 儲存
- 詳細日誌
- 模組日誌
- 正在保存日誌,請稍候
- 日誌已儲存
- 儲存失敗:\n%s
- 立即清理日誌
- 日誌清理成功
- 移至頂端
- 正在載入……
- 移至底端
- 重新載入
- 日誌清理失敗
- 自動換行
- 詳細日誌已啟用
- 詳細日誌已禁用
-
- (未提供介紹)
- 該模組需要更新版本的 Xposed(%d),因此無法被啟用
- 此模組專為較新的 Xposed 版本 (%d) 而設計,因此某些功能可能無法運作
- 該模組未指定需要的 Xposed 版本
- 此模組適用於 %1$d 版本的 Xposed ,由於版本 %2$d 的變更不相容,因此已經停用此模組
- 由於此模組被安裝在SD卡中而無法載入,請將其移動到內部儲存空間
- 解除安裝
- 模組設定
- 在倉庫中檢視
- 您確定要移除此模組嗎?
- 已移除 %1$s
- 移除失敗
- 為使用者安裝模組
- 已為使用者 %2$s 安裝模組 %1$s
- 模組安裝失敗
- 為使用者 %s 安裝
- 確定要為使用者 %2$s 安裝 %1$s 嗎?建議手動安裝或多開,透過 LSPosed 強制安裝可能會出現問題。
- 展開
- 收起
-
- 重新最佳化
- 正在最佳化……
- 最佳化完成
- 執行
- 最佳化失敗或返回值為空
- 最佳化失敗:
- 程式名稱
- 套件名稱
- 安裝時間
- 更新時間
- 遞減
- 系統程式
- 排序
- 啟用模組
- 未選擇任何程式,是否繼續?
- 遊戲
- 模組
- 作用域列表儲存失敗
- 版本:%1$s
- 推薦程式
- 未選擇任何程式,選擇推薦的程式?
- 選擇推薦的程式?
- Xposed 模組尚未啟用
- 推薦啟用
- 可用更新:%1$s
- 由於未選擇任何程式,模組 %s 已被停用。
- 系統框架
- 備份
- 備份
- 還原
- 強制停止
- 確定要強制停止?
- 如果您強制停止應用程式,可能導致行為異常。
- 需要重新啟動以套用此變更
- 重啟系統
- 隱藏
-
- 在其他應用程式中檢視
- 程式資訊
- ¯\_(ツ)_/¯\n空空如也
-
- 框架
- 停用詳細日誌
- 回報問題要求包含詳細日誌
- 黑色主題
- 當深色主題啟用時使用純黑色主題
- 主題
- 備份與還原
- 備份模組列表和作用域清單
- 還原模組列表和作用域清單
- 備份
- 備份失敗:\n%s
- 請啟用 DocumentUI
- 還原
- 還原失敗:\n%s
- 網路
- 安全 DNS(DoH)
- 解決部分地區的 DNS 中毒問題
- 主題強調色
- 系統主題顏色
- 強制應用程式在啟動器中顯示圖示
- 在 Android 10 之後,應用(特別是 Xposed 模組)不被允許隱藏啟動器圖示。關閉本選項以停用此功能。
- 系統
- 語言
- 譯者
- 參與翻譯
- 幫助我們翻譯 %s 到您的語言
- 建立可以開啟寄生管理員的捷徑
- 捷徑已釘選
- 目前的預設啟動器不支援釘選捷徑
- 狀態通知
- 顯示通知以便開啟寄生管理員
- 更新通道
- 穩定版
- 測試版
- 每夜構建
-
- 自述檔案
- 版本
- 資訊
- 首頁
- 原始碼
- 合作者
- 附件
- 在瀏覽器中開啟
- 顯示較舊的版本
- 沒有更舊的版本
- 模組倉庫載入失敗:%s
- 可更新的優先
- 已安裝
-
- - %d 次下載
-
-
- 櫻花
- 紅色
- 粉色
- 紫色
- 深紫
- 靛青
- 藍色
- 淺藍
- 青色
- 青綠
- 綠色
- 淺綠
- 萊姆綠
- 黃色
- 琥珀
- 橙色
- 深橙
- 棕色
- 藍灰
-
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
deleted file mode 100644
index 2ddb9495a..000000000
--- a/app/src/main/res/values/arrays.xml
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
- - @string/dark_theme_off
- - @string/dark_theme_on
- - @string/dark_theme_follow_system
-
-
-
- - MODE_NIGHT_NO
- - MODE_NIGHT_YES
- - MODE_NIGHT_FOLLOW_SYSTEM
-
-
-
- - SAKURA
- - MATERIAL_RED
- - MATERIAL_PINK
- - MATERIAL_PURPLE
- - MATERIAL_DEEP_PURPLE
- - MATERIAL_INDIGO
- - MATERIAL_BLUE
- - MATERIAL_LIGHT_BLUE
- - MATERIAL_CYAN
- - MATERIAL_TEAL
- - MATERIAL_GREEN
- - MATERIAL_LIGHT_GREEN
- - MATERIAL_LIME
- - MATERIAL_YELLOW
- - MATERIAL_AMBER
- - MATERIAL_ORANGE
- - MATERIAL_DEEP_ORANGE
- - MATERIAL_BROWN
- - MATERIAL_BLUE_GREY
-
-
-
- - @string/color_sakura
- - @string/color_red
- - @string/color_pink
- - @string/color_purple
- - @string/color_deep_purple
- - @string/color_indigo
- - @string/color_blue
- - @string/color_light_blue
- - @string/color_cyan
- - @string/color_teal
- - @string/color_green
- - @string/color_light_green
- - @string/color_lime
- - @string/color_yellow
- - @string/color_amber
- - @string/color_orange
- - @string/color_deep_orange
- - @string/color_brown
- - @string/color_blue_grey
-
-
-
- - @string/update_channel_stable
- - @string/update_channel_bate
- - @string/update_channel_nightly
-
-
-
- - CHANNEL_STABLE
- - CHANNEL_BETA
- - CHANNEL_NIGHTLY
-
-
-
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
deleted file mode 100644
index 6ac935b58..000000000
--- a/app/src/main/res/values/attrs.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
deleted file mode 100644
index bb4fe7561..000000000
--- a/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- @color/abc_primary_text_material_dark
-
- @color/abc_primary_text_material_light
-
- #FFFFFF
- #F48FB1
-
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
deleted file mode 100644
index 25297efd3..000000000
--- a/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- 48dp
- 48dp
-
- 6dp
-
diff --git a/app/src/main/res/values/integer.xml b/app/src/main/res/values/integer.xml
deleted file mode 100644
index f12b67bae..000000000
--- a/app/src/main/res/values/integer.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- 0x00800007
- 0x30
- 0
-
diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml
deleted file mode 100644
index bd53f0275..000000000
--- a/app/src/main/res/values/settings.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- false
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
deleted file mode 100644
index 469a63577..000000000
--- a/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,253 +0,0 @@
-
-
-
-
- Overview
- Modules
-
- - %d module enabled
- - %d modules enabled
-
- Logs
- Settings
- Feedback or suggestion
- About
- Report issue
- Repository
- All modules up to date
- Published at %s
- Updated at %s
-
- - %d module upgradable
- - %d modules upgradable
-
- Join our %2$s channel]]>
- null
- Install
- Tap to install LSPosed
- Not installed
- LSPosed is not Installed
- Activated
- Partially activated
- SEPolicy is not loaded properly
- Please report this to Magisk developer.]]>
- System Framework injection failed
- Magisk or some low-quality Magisk modules. Please try to disable Magisk modules other than Riru and LSPosed or submit full log to developers.]]>
- System prop incorrect
- Modules may invalidate occasionally.]]>
- Need to update
- Please install the latest version of LSPosed
- Tips for module developer
- Please disable deploy optimizations on Android Studio, or use `gradlew installDebug` command to install. Otherwise the module apk will not be updated.
- API version
- Framework version
- Manager package name
- System version
- Device
- System ABI
- Dex Optimizer Wrapper
- Enabled
- Not enabled
- Supported
- Unsupported
- Android version unsatisfied
- Crashed
- Mount failed
- SELinux is permissive
- SELinux policy is incorrect
- Update LSPosed
- Confirm to update LSPosed? This device will reboot after update completion
- Copied to clipboard
-
- Welcome to LSPosed
- You are using the parasitic manager, which can create shortcut or still open from notification.
- You are using the parasitic manager, which can open from notification.
- Create shortcut
- Never show
- Parasitic Manager Recommended
- LSPosed now supports system parasitization to avoid detection, you can open parasitic manager from notification. It is recommended to uninstall the current application.
-
- Save
- Verbose Logs
- Modules Logs
- Saving log, please wait
- Logs saved
- Failed to save:\n%s
- Clear log now
- Log successfully cleared.
- Scroll to top
- Loading…
- Scroll to bottom
- Reload
- Failed to clear the log
- Word Wrap
- Verbose log enabled
- Verbose log disabled
-
- (no description provided)
- This module requires a newer Xposed version (%d) and thus cannot be activated
- This module is designed for a newer Xposed version (%d) and thus some functionalities may not work
- This module does not specify the Xposed version it needs.
- API %1$d
- API ?
- Xposed %1$d
- Xposed ?
- Targets the API this framework implements
- Targets API %1$d, older than the API this framework implements (%2$d)
- Targets API %1$d, newer than the API this framework implements (%2$d)
- Needs API %1$d, more than this framework implements (%2$d)
- Uses the original Xposed API through the legacy bridge
- The targeted API version is unknown
- Static Scope
- This module was created for Xposed version %1$d, but due to incompatible changes in version %2$d, it has been disabled
- This module cannot be loaded because it\'s installed on the SD card, please move it to internal storage
- Uninstall
- Module settings
- View in Repo
- Do you want to uninstall this module?
- Uninstalled %1$s
- Uninstall unsuccessful
- Add module to user
- Added %1$s to user %2$s
- Adding module failed
- Install to user %s
- Want to install %1$s to user %2$s? It is recommended to install manually, forcing installation via LSPosed may cause problems.
- expand
- collapse
-
- Re-optimize
- Optimizing…
- Optimization complete
- Launch it
- Optimization failed: return value is empty
- Optimization failed:
- Application name
- Package name
- Install time
- Update time
- Reverse
- System apps
- Sorting
- Enable module
- You did not select any app. Continue?
- Games
- Modules
- Failed to save scope list
- Version: %1$s
- Select
- Recommended
- You did not select any app. Select recommended apps?
- Select recommended apps?
- All
- None
- Auto-Include
- Xposed module is not activated yet
- Recommended
- Update available: %1$s
- Module %s has been disabled since no app selected.
- System Framework
- Backup
- Backup
- Restore
- Force stop
- Force stop?
- If you force stop an app, it may misbehave.
- Reboot is required for this change to apply
- Reboot
- Hide
-
- View in other app
- App info
- ¯\\_(ツ)_\/¯\nNothing here
-
- Framework
- Disable verbose logs
- Verbose logs are required to report issues
- Black dark theme
- Use the pure black theme if dark theme is enabled
- Theme
- Backup and restore
- Backup module list and scope lists.
- Restore module list and scope lists.
- Backup
- Failed to backup:\n%s
- Please enable DocumentUI
- Restore
- Failed to restore:\n%s
- Network
- DNS over HTTPS
- Workaround DNS poisoning in some nations
- Theme color
- System theme color
- Force apps to show launcher icons
- After Android 10, apps are not allowed to hide their launcher icons. Turn off the toggle to disable this system feature.
- System
- Language
- Translation contributors
- Participate in translation
- Help us translate %s into your language
- Create a shortcut that can open parasitic manager
- Shortcut pinned
- The current default launcher does not support pin shortcuts
- Status Notification
- Show a notification that can open parasitic manager
- Update channel
- Stable
- Beta
- Nightly build
-
- Readme
- Releases
- Info
- Homepage
- Source code
- Collaborators
- Assets
- Open in browser
- Show older versions
- No more release
- Failed to load module repo: %s
- Upgradable first
- Installed
-
- - %d download
- - %d downloads
-
-
- Sakura
- Red
- Pink
- Purple
- Deep purple
- Indigo
- Blue
- Light blue
- Cyan
- Teal
- Green
- Light green
- Lime
- Yellow
- Amber
- Orange
- Deep orange
- Brown
- Blue grey
-
diff --git a/app/src/main/res/values/strings_untranslatable.xml b/app/src/main/res/values/strings_untranslatable.xml
deleted file mode 100644
index b6264e42d..000000000
--- a/app/src/main/res/values/strings_untranslatable.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
- LSPosed
- https://github.com/JingMatrix/LSPosed#install
- https://github.com/JingMatrix/LSPosed/releases/latest
- @string/module_repo
-
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index 3597fa5c4..000000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
deleted file mode 100644
index 06810bdd8..000000000
--- a/app/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/themes_custom.xml b/app/src/main/res/values/themes_custom.xml
deleted file mode 100644
index 2e3cd58e6..000000000
--- a/app/src/main/res/values/themes_custom.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/themes_overlay.xml b/app/src/main/res/values/themes_overlay.xml
deleted file mode 100644
index 3512ffe8f..000000000
--- a/app/src/main/res/values/themes_overlay.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/themes_override.xml b/app/src/main/res/values/themes_override.xml
deleted file mode 100644
index ee230131e..000000000
--- a/app/src/main/res/values/themes_override.xml
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/xml/prefs.xml b/app/src/main/res/xml/prefs.xml
deleted file mode 100644
index 35511c6b3..000000000
--- a/app/src/main/res/xml/prefs.xml
+++ /dev/null
@@ -1,147 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
deleted file mode 100644
index 8f37ef11d..000000000
--- a/app/src/main/res/xml/shortcuts.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/build.gradle.kts b/build.gradle.kts
index 290706f9c..422e8d634 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,6 +11,11 @@ import org.gradle.process.ExecOperations
plugins {
alias(libs.plugins.agp.lib) apply false
alias(libs.plugins.agp.app) apply false
+ // Declaring the Kotlin plugin here pins the version on the buildscript classpath for
+ // every module. AGP 9 otherwise supplies its own, older Kotlin, and a module that
+ // asks for a specific version fails with "already on the classpath with an unknown
+ // version". The Compose stack in :manager needs the newer compiler.
+ alias(libs.plugins.kotlin) apply false
alias(libs.plugins.ktfmt)
}
@@ -54,13 +59,51 @@ abstract class GitLatestTagValueSource : ValueSource {
+ @get:Inject abstract val execOperations: ExecOperations
+
+ override fun obtain(): String {
+ val hash = ByteArrayOutputStream()
+ val hashResult = execOperations.exec {
+ commandLine("git", "rev-parse", "--short", "HEAD")
+ standardOutput = hash
+ isIgnoreExitValue = true
+ }
+ if (hashResult.exitValue != 0 || hash.toString().isBlank()) return "unknown"
+
+ // A dirty tree is the case worth naming: that build corresponds to no commit at all, so
+ // "which commit is this" has no answer and the version should say so rather than name a
+ // commit the binary does not match.
+ val dirty = ByteArrayOutputStream()
+ val dirtyResult = execOperations.exec {
+ commandLine("git", "status", "--porcelain", "--untracked-files=no")
+ standardOutput = dirty
+ isIgnoreExitValue = true
+ }
+ val suffix = if (dirtyResult.exitValue == 0 && dirty.toString().isNotBlank()) "-dirty" else ""
+ return hash.toString().trim() + suffix
+ }
+}
+
// This defers the execution of the git commands and allows Gradle to cache the results.
val versionCodeProvider by extra(providers.of(GitCommitCountValueSource::class.java) {})
+val versionHashProvider by extra(providers.of(GitCommitHashValueSource::class.java) {})
val versionNameProvider by extra(providers.of(GitLatestTagValueSource::class.java) {})
val injectedPackageName by extra("com.android.shell")
val injectedPackageUid by extra(2000)
-val defaultManagerPackageName by extra("org.lsposed.manager")
+val defaultManagerPackageName by extra("org.matrix.vector.manager")
val androidTargetSdkVersion by extra(37)
val androidMinSdkVersion by extra(27)
@@ -165,6 +208,7 @@ tasks.register("format") {
// :daemon:ktfmtFormat, which formats the daemon (scripts included) in Meta style.
exclude("daemon/**")
dependsOn(":daemon:ktfmtFormat")
+ dependsOn(":manager:ktfmtFormat")
dependsOn(":xposed:ktfmtFormat")
dependsOn(":zygisk:ktfmtFormat")
}
diff --git a/crowdin.yml b/crowdin.yml
index 17fa5c1f1..ff983868f 100644
--- a/crowdin.yml
+++ b/crowdin.yml
@@ -5,11 +5,12 @@ base_url: 'https://api.crowdin.com'
pull_request_title: '[translation] Update translation from Crowdin'
preserve_hierarchy: 1
files:
- - source: /app/src/main/res/values/strings.xml
- translation: /app/src/main/res/values-%two_letters_code%/%original_file_name%
+ # %android_code% rather than %two_letters_code%: it is the only placeholder that produces the
+ # region-qualified resource folders Android actually uses — values-zh-rCN, values-pt-rBR — and
+ # with the two-letter form Crowdin cannot even find the existing ones to upload them.
+ - source: /manager/src/main/res/values/strings*.xml
+ translation: /manager/src/main/res/values-%android_code%/%original_file_name%
type: android
- dest: /app/strings.xml
- source: /daemon/src/main/res/values/strings.xml
- translation: /daemon/src/main/res/values-%two_letters_code%/%original_file_name%
+ translation: /daemon/src/main/res/values-%android_code%/%original_file_name%
type: android
- dest: /daemon/strings.xml
diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts
index 1b8ceda06..07c739f84 100644
--- a/daemon/build.gradle.kts
+++ b/daemon/build.gradle.kts
@@ -16,6 +16,7 @@ val injectedPackageName: String by rootProject.extra
val injectedPackageUid: Int by rootProject.extra
val versionCodeProvider: Provider by rootProject.extra
val versionNameProvider: Provider by rootProject.extra
+val versionHashProvider: Provider by rootProject.extra
plugins {
alias(libs.plugins.agp.app)
@@ -23,35 +24,40 @@ plugins {
}
android {
- defaultConfig {
- buildConfigField(
- "String",
- "DEFAULT_MANAGER_PACKAGE_NAME",
- """"$defaultManagerPackageName"""",
- )
- buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""")
- buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""")
- buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""")
- buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""")
- buildConfigField("long", "VERSION_CODE", versionCodeProvider.get())
-
- val cliToken = UUID.randomUUID()
- // Inject the MSB and LSB as Long constants
- buildConfigField("Long", "CLI_TOKEN_MSB", "${cliToken.mostSignificantBits}L")
- buildConfigField("Long", "CLI_TOKEN_LSB", "${cliToken.leastSignificantBits}L")
- }
+ defaultConfig {
+ buildConfigField(
+ "String",
+ "DEFAULT_MANAGER_PACKAGE_NAME",
+ """"$defaultManagerPackageName"""",
+ )
+ buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""")
+ buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""")
+ buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""")
+ buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""")
+ buildConfigField("long", "VERSION_CODE", versionCodeProvider.get())
+ // The version code is the commit count on origin/master, so it is identical for a branch
+ // build and a master build. The hash is what tells a bug report which one it came from.
+ buildConfigField("String", "VERSION_HASH", """"${versionHashProvider.get()}"""")
+
+ val cliToken = UUID.randomUUID()
+ // Inject the MSB and LSB as Long constants
+ buildConfigField("Long", "CLI_TOKEN_MSB", "${cliToken.mostSignificantBits}L")
+ buildConfigField("Long", "CLI_TOKEN_LSB", "${cliToken.leastSignificantBits}L")
+ }
- buildTypes {
- all { externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } } }
- release {
- isMinifyEnabled = true
- proguardFiles("proguard-rules.pro")
+ buildTypes {
+ all {
+ externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } }
+ }
+ release {
+ isMinifyEnabled = true
+ proguardFiles("proguard-rules.pro")
+ }
}
- }
- externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } }
+ externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } }
- namespace = "org.matrix.vector.daemon"
+ namespace = "org.matrix.vector.daemon"
}
/**
@@ -107,10 +113,10 @@ androidComponents {
val signInfoTask =
tasks.register("generate${variantCapped}SignInfo") {
- dependsOn(":app:validateSigning${variantCapped}")
+ dependsOn(":manager:validateSigning${variantCapped}")
val sign =
rootProject
- .project(":app")
+ .project(":manager")
.extensions
.getByType(ApplicationExtension::class.java)
.buildTypes
@@ -132,15 +138,15 @@ androidComponents {
}
dependencies {
- implementation(libs.agp.apksig)
- implementation(libs.gson)
- implementation(libs.picocli)
- implementation(libs.kotlinx.coroutines.android)
- implementation(libs.kotlinx.coroutines.core)
- implementation(projects.external.apache)
- implementation(projects.hiddenapi.bridge)
- implementation(projects.services.daemonService)
- implementation(projects.services.managerService)
- compileOnly(libs.androidx.annotation)
- compileOnly(projects.hiddenapi.stubs)
+ implementation(libs.agp.apksig)
+ implementation(libs.gson)
+ implementation(libs.picocli)
+ implementation(libs.kotlinx.coroutines.android)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(projects.external.apache)
+ implementation(projects.hiddenapi.bridge)
+ implementation(projects.services.daemonService)
+ implementation(projects.services.managerService)
+ compileOnly(libs.androidx.annotation)
+ compileOnly(projects.hiddenapi.stubs)
}
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt
index 9c5224d82..08ec3d6b1 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt
@@ -62,7 +62,12 @@ object VectorDaemon {
}
Log.i(TAG, "Vector daemon started: lateInject=$isLateInject, proxy=$proxyServiceName")
- Log.i(TAG, "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
+ // The hash is here rather than in the version the manager prints: Home should stay readable,
+ // but every saved bug report should say exactly which commit produced the daemon that wrote it.
+ Log.i(
+ TAG,
+ "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) " +
+ "commit ${BuildConfig.VERSION_HASH}")
Thread.setDefaultUncaughtExceptionHandler { _, e ->
Log.e(TAG, "Uncaught exception in Daemon", e)
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt
index bcbeb3ecc..49fdd6ef7 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt
@@ -487,6 +487,39 @@ object FileSystem {
return "${prefix}_${formatter.format(Instant.now())}.log"
}
+ /**
+ * The parts still on disk for one of the two logs, oldest first.
+ *
+ * Read from the directory rather than from LogcatMonitor's LRU so that a manager opened after a
+ * daemon restart still sees the history: the LRU is rebuilt empty, the files are not.
+ */
+ fun listLogParts(verbose: Boolean): List {
+ val prefix = if (verbose) "verbose_" else "modules_"
+ return runCatching {
+ logDirPath
+ .toFile()
+ .listFiles { file -> file.isFile && file.name.startsWith(prefix) && file.name.endsWith(".log") }
+ ?.map { it.name }
+ // The names carry an ISO-8601 timestamp, so lexicographic order is chronological.
+ ?.sorted()
+ .orEmpty()
+ }
+ .getOrDefault(emptyList())
+ }
+
+ /**
+ * Opens one part by name.
+ *
+ * The name arrives from an unprivileged process and is used to build a path inside a directory
+ * only root can read, so it is never trusted: it has to be one of the names [listLogParts] just
+ * returned, which rules out traversal and anything outside the log directory by construction
+ * rather than by pattern-matching for "..".
+ */
+ fun openLogPart(verbose: Boolean, name: String): File? {
+ if (name !in listLogParts(verbose)) return null
+ return logDirPath.resolve(name).toFile().takeIf { it.isFile }
+ }
+
fun getNewVerboseLogPath(): File {
createLogDirPath()
return logDirPath.resolve(getNewLogFileName("verbose")).toFile()
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt
index 134c80ea8..4d1380ae9 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt
@@ -69,9 +69,17 @@ object ModuleDatabase {
val values = ContentValues().apply { put("mid", mid) }
for (app in scope) {
- if (app.packageName == "system" && app.userId != 0) continue
+ // The system server is one process for the whole device, so it is stored against user 0
+ // whoever asked for it — a module in a work profile hooking the framework is hooking the
+ // same system_server as everyone else.
+ //
+ // Normalised rather than dropped, which is what this used to do. Dropping meant restoring
+ // a backup written by an older manager, which recorded the framework under the module's
+ // own user, silently lost the one target the module may have cared about — and said
+ // nothing about it.
+ val userId = if (app.packageName == "system") 0 else app.userId
values.put("app_pkg_name", app.packageName)
- values.put("user_id", app.userId)
+ values.put("user_id", userId)
db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE)
}
db.setTransactionSuccessful()
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt
index 7cc849980..ccfbd0c4e 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt
@@ -23,6 +23,7 @@ import hidden.HiddenApiBridge
import io.github.libxposed.service.IXposedService
import java.io.File
import java.util.concurrent.CountDownLatch
+import org.lsposed.lspd.IFrameworkInstallCallback
import org.lsposed.lspd.ILSPManagerService
import org.lsposed.lspd.models.Application
import org.lsposed.lspd.models.UserInfo
@@ -35,6 +36,7 @@ import org.matrix.vector.daemon.env.Dex2OatServer
import org.matrix.vector.daemon.env.LogcatMonitor
import org.matrix.vector.daemon.system.*
import org.matrix.vector.daemon.utils.PackageOptimizer
+import org.matrix.vector.daemon.utils.RootImplementation
import org.matrix.vector.daemon.utils.applyXspaceWorkaround
import org.matrix.vector.daemon.utils.getRealUsers
import rikka.parcelablelist.ParcelableListSlice
@@ -147,7 +149,7 @@ object ManagerService : ILSPManagerService.Stub() {
}
intent.categories?.clear()
- intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER")
+ intent.addCategory("${BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME}.LAUNCH_MANAGER")
intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME)
managerIntent = Intent(intent)
}
@@ -214,6 +216,8 @@ object ManagerService : ILSPManagerService.Stub() {
override fun getXposedVersionName() = BuildConfig.VERSION_NAME
+ override fun getFrameworkCommit(): String? = BuildConfig.VERSION_HASH.takeIf { it.isNotBlank() }
+
override fun getInstalledPackagesFromAllUsers(
flags: Int,
filterNoProcess: Boolean
@@ -233,13 +237,25 @@ object ManagerService : ILSPManagerService.Stub() {
override fun getModuleScope(packageName: String) = ConfigCache.getModuleScope(packageName)
- override fun isVerboseLog() = PreferenceStore.isVerboseLogEnabled() || BuildConfig.DEBUG
+ // Reports the setting, not the setting OR'd with the build type. It used to be
+ // `|| BuildConfig.DEBUG`, which made the value unwritable on a debug daemon: the manager could
+ // never read false, so its switch snapped back on every tap and had to be greyed out. The OR was
+ // redundant anyway — `isVerboseLogEnabled()` already defaults to true — so a debug build still
+ // logs verbosely out of the box, and now a developer can also turn it off.
+ override fun isVerboseLog() = PreferenceStore.isVerboseLogEnabled()
override fun setVerboseLog(enabled: Boolean) {
PreferenceStore.setVerboseLog(enabled)
if (isVerboseLog()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose()
}
+ override fun getLogParts(verbose: Boolean): List = FileSystem.listLogParts(verbose)
+
+ override fun getLogPart(verbose: Boolean, name: String): ParcelFileDescriptor? =
+ FileSystem.openLogPart(verbose, name)?.let {
+ ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY)
+ }
+
override fun getVerboseLog() =
LogcatMonitor.getVerboseLog()?.let {
ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY)
@@ -423,4 +439,32 @@ object ManagerService : ILSPManagerService.Stub() {
ModuleDatabase.setAutoInclude(packageName, enabled)
override fun getAutoInclude(packageName: String) = ConfigCache.getAutoInclude(packageName)
+
+ override fun getRootImplementation() = RootImplementation.implementation
+
+ override fun getRootImplementationVersion() = RootImplementation.version
+
+ override fun installFrameworkZip(zipPath: String, callback: IFrameworkInstallCallback) {
+ // Off the binder thread: a flash takes seconds to minutes, and holding a binder thread for its
+ // duration starves everything else the manager asks of the daemon meanwhile — including the
+ // log reads the install screen is doing to show what is happening.
+ Thread {
+ val exit =
+ RootImplementation.install(zipPath) { line ->
+ runCatching { callback.onLine(line) }
+ .onFailure {
+ // The manager went away mid-flash. Keep installing — stopping now would
+ // leave the module tree half-written — and keep logging, which is the only
+ // record left.
+ Log.w(TAG, "Install callback is gone; continuing", it)
+ }
+ }
+ runCatching { callback.onFinished(exit) }
+ .onFailure { Log.w(TAG, "Could not report install result", it) }
+ }
+ .apply {
+ name = "vector-framework-install"
+ start()
+ }
+ }
}
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt
index 56e9a80c2..f05a89233 100644
--- a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt
@@ -85,7 +85,7 @@ object NotificationManager {
}
private fun getNotificationIcon(): Icon {
- return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification))
+ return Icon.createWithBitmap(getBitmap(R.drawable.ic_statue_monochrome))
}
fun notifyStatusNotification() {
diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/RootImplementation.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/RootImplementation.kt
new file mode 100644
index 000000000..050089e0a
--- /dev/null
+++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/RootImplementation.kt
@@ -0,0 +1,228 @@
+package org.matrix.vector.daemon.utils
+
+import android.util.Log
+import java.io.BufferedReader
+import java.io.File
+import java.io.InputStreamReader
+import org.lsposed.lspd.ILSPManagerService
+
+private const val TAG = "VectorRootInstaller"
+
+/**
+ * Which root implementation is managing this device, and how to flash through it.
+ *
+ * The detection mirrors NeoZygisk's `root_impl` module, deliberately: that is the code deciding
+ * whether Vector loads at all on this device, and a manager that disagreed with it about which root
+ * is in charge would be reporting on a different device than the one it is running on. Where a
+ * version floor can be read at all it is NeoZygisk's own, so "too old to flash through" means the
+ * same thing in both places.
+ *
+ * Detection is by binary rather than by NeoZygisk's ioctl/prctl route, which needs a JNI hop for a
+ * question asked once — and the binary has to exist anyway to do the flashing. The cost of that
+ * choice is visible in [detectKernelSu], which cannot read a version at all.
+ *
+ * A binary that exists but *fails* is not an implementation: this device carries a leftover
+ * `/data/adb/magisk/magisk` from a previous root manager, and it exits 1 with "Cannot connect to
+ * daemon". Requiring a clean exit is what stops that from being reported as a second root
+ * implementation and turning a working KernelSU device into ROOT_MULTIPLE.
+ */
+object RootImplementation {
+
+ /**
+ * NeoZygisk's floors, from its root build.gradle.kts. Below these it will not load.
+ *
+ * There is deliberately no KernelSU floor here: its version code is not reachable from a shell,
+ * and [detectKernelSu] explains why not checking it is correct rather than merely convenient.
+ */
+ private const val MIN_MAGISK = 26402
+ private const val MIN_APATCH = 10762
+
+ /**
+ * Where each implementation keeps its binary.
+ *
+ * Tried by absolute path as well as by name because the daemon does not inherit a login shell's
+ * PATH: it is started by the root implementation's own init stage, and on some of them PATH holds
+ * nothing but /system/bin. Falling back to the well-known locations turns "we could not detect
+ * root" into "there really is no root" for the cases that matter.
+ */
+ private val MAGISK_PATHS = listOf("magisk", "/data/adb/magisk/magisk")
+ private val KSUD_PATHS = listOf("ksud", "/data/adb/ksud")
+ private val APD_PATHS = listOf("apd", "/data/adb/apd")
+
+ /** Detected once: three process spawns is not something to repeat on every screen open. */
+ private val detected: Detection by lazy { detect() }
+
+ /**
+ * [binary] is the path detection actually got an answer from, and the one the flash then uses.
+ *
+ * Not re-derived at install time: `ksud` may be on the daemon's PATH or only at /data/adb/ksud,
+ * and the version probe already established which. Guessing again invites the flash to fail on a
+ * device where detection succeeded.
+ */
+ data class Detection(val implementation: Int, val version: String?, val binary: String? = null)
+
+ val implementation: Int
+ get() = detected.implementation
+
+ val version: String?
+ get() = detected.version
+
+ private fun detect(): Detection {
+ val magisk = detectMagisk()
+ val ksu = detectKernelSu()
+ val apatch = detectApatch()
+
+ val found = listOfNotNull(magisk, ksu, apatch)
+ if (found.size > 1) {
+ // Not a failure to detect — a device with two root implementations installed, where
+ // flashing through either is a coin toss about which one owns the module tree.
+ Log.w(TAG, "Multiple root implementations: ${found.joinToString { it.version ?: "?" }}")
+ return Detection(ILSPManagerService.ROOT_MULTIPLE, found.joinToString { it.version ?: "?" })
+ }
+
+ val only = found.firstOrNull() ?: return Detection(ILSPManagerService.ROOT_NONE, null)
+ Log.i(TAG, "Root implementation: ${only.version} via ${only.binary}")
+ return only
+ }
+
+ /** Null when this implementation is not present; otherwise which it is and where it lives. */
+ private fun detectMagisk(): Detection? {
+ val (binary, raw) = run(MAGISK_PATHS, "-V") ?: return null
+ val code = raw.trim().toIntOrNull() ?: return null
+ val name = run(MAGISK_PATHS, "-v")?.second?.trim()?.lineSequence()?.firstOrNull()
+ val supported = code >= MIN_MAGISK
+ return Detection(
+ if (supported) ILSPManagerService.ROOT_MAGISK else ILSPManagerService.ROOT_TOO_OLD,
+ "Magisk ${name ?: code}",
+ binary,
+ )
+ }
+
+ /**
+ * KernelSU, which cannot be version-checked from a shell.
+ *
+ * `ksud -V` prints a *build hash*, not a version code — measured on a KernelSU device it answers
+ * `ksud 64e3761d`. An earlier version of this took the first run of digits out of that, read
+ * `64`, compared it against the 10940 floor and declared the device too old to flash on — which
+ * would have disabled the entire feature on exactly the devices it works on. The version code
+ * lives behind KernelSU's prctl/ioctl interface, which is why NeoZygisk reaches for it and why
+ * this cannot.
+ *
+ * So presence is the whole test, and that is sound rather than a shrug: NeoZygisk refuses to load
+ * on a KernelSU older than its floor, so a daemon that is running at all is running under one new
+ * enough. The check this cannot perform has already been performed, one layer down.
+ */
+ private fun detectKernelSu(): Detection? {
+ val (binary, raw) = run(KSUD_PATHS, "-V") ?: return null
+ val build = raw.trim().substringAfter("ksud ").trim()
+ return Detection(ILSPManagerService.ROOT_KERNELSU, "KernelSU ($build)", binary)
+ }
+
+ /**
+ * APatch. `apd -V` prints "apd ", so the second field is the version — NeoZygisk's parse.
+ *
+ * When that field is not a number, this reports the implementation as present and usable rather
+ * than absent. Refusing to flash because *our parser* did not recognise a version string would be
+ * refusing on the evidence of our own code rather than on the state of the device — which is the
+ * mistake the KernelSU branch above was making.
+ */
+ private fun detectApatch(): Detection? {
+ val (binary, raw) = run(APD_PATHS, "-V") ?: return null
+ val output = raw.trim()
+ val code = output.split(Regex("\\s+")).getOrNull(1)?.toIntOrNull()
+ return when {
+ code == null -> Detection(ILSPManagerService.ROOT_APATCH, "APatch ($output)", binary)
+ code >= MIN_APATCH -> Detection(ILSPManagerService.ROOT_APATCH, "APatch $code", binary)
+ else -> Detection(ILSPManagerService.ROOT_TOO_OLD, "APatch $code", binary)
+ }
+ }
+
+ /**
+ * First candidate that starts and exits cleanly wins, returned with the path that worked.
+ *
+ * A non-zero exit reads as absent, which is what keeps a stale Magisk binary from a previous root
+ * manager out of the results.
+ */
+ private fun run(candidates: List, vararg args: String): Pair? {
+ for (path in candidates) {
+ val result =
+ runCatching {
+ val process = ProcessBuilder(listOf(path) + args).redirectErrorStream(false).start()
+ val output =
+ BufferedReader(InputStreamReader(process.inputStream)).use { it.readText() }
+ if (process.waitFor() == 0 && output.isNotBlank()) path to output else null
+ }
+ .getOrNull()
+ if (result != null) return result
+ }
+ return null
+ }
+
+ /**
+ * The command that installs a module zip, for the implementation in charge.
+ *
+ * These are the same three the project's gradle install tasks use, so a zip that flashes from a
+ * developer's machine flashes the same way from the device.
+ */
+ private fun installCommand(zipPath: String): List? {
+ val binary = detected.binary ?: return null
+ return when (implementation) {
+ ILSPManagerService.ROOT_MAGISK -> listOf(binary, "--install-module", zipPath)
+ ILSPManagerService.ROOT_KERNELSU -> listOf(binary, "module", "install", zipPath)
+ ILSPManagerService.ROOT_APATCH -> listOf(binary, "module", "install", zipPath)
+ else -> null
+ }
+ }
+
+ /**
+ * Runs the installer, handing every line to [onLine] and to the daemon's log.
+ *
+ * Both, not either: the screen is where a user reads a failure, and the log is where a
+ * maintainer reads it afterwards from a bug report — including the case where the flash left the
+ * device unable to boot the manager at all.
+ *
+ * Blocks until the installer exits. The caller runs it off the binder thread.
+ */
+ fun install(zipPath: String, onLine: (String) -> Unit): Int {
+ val zip = File(zipPath)
+ if (!zip.isFile || !zip.canRead()) {
+ val message = "Refusing to flash $zipPath: not a readable file"
+ Log.e(TAG, message)
+ onLine(message)
+ return ILSPManagerService.INSTALL_NO_SUCH_FILE
+ }
+
+ val command =
+ installCommand(zipPath)
+ ?: run {
+ val message = "No usable root implementation to flash through (code $implementation)"
+ Log.e(TAG, message)
+ onLine(message)
+ return ILSPManagerService.INSTALL_NO_ROOT
+ }
+
+ Log.i(TAG, "Flashing ${zip.name} with: ${command.joinToString(" ")}")
+ onLine("$ ${command.joinToString(" ")}")
+
+ return runCatching {
+ // Merged, because an installer's diagnostics go to stderr and its progress to stdout,
+ // and reading them on two threads would interleave them in an order that is not the
+ // order they happened in.
+ val process = ProcessBuilder(command).redirectErrorStream(true).start()
+ BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
+ reader.lineSequence().forEach { line ->
+ Log.i(TAG, line)
+ onLine(line)
+ }
+ }
+ val exit = process.waitFor()
+ Log.i(TAG, "Installer exited with $exit")
+ exit
+ }
+ .getOrElse {
+ Log.e(TAG, "Installer could not be started", it)
+ onLine("Could not start the installer: ${it.message}")
+ ILSPManagerService.INSTALL_NOT_EXECUTED
+ }
+ }
+}
diff --git a/daemon/src/main/res/drawable/ic_baseline_block_24.xml b/daemon/src/main/res/drawable/ic_baseline_block_24.xml
deleted file mode 100644
index 1e478d2aa..000000000
--- a/daemon/src/main/res/drawable/ic_baseline_block_24.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/daemon/src/main/res/drawable/ic_baseline_check_24.xml b/daemon/src/main/res/drawable/ic_baseline_check_24.xml
deleted file mode 100644
index cf143d4d5..000000000
--- a/daemon/src/main/res/drawable/ic_baseline_check_24.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/daemon/src/main/res/drawable/ic_baseline_close_24.xml b/daemon/src/main/res/drawable/ic_baseline_close_24.xml
deleted file mode 100644
index 844b6b62e..000000000
--- a/daemon/src/main/res/drawable/ic_baseline_close_24.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/daemon/src/main/res/drawable/ic_notification.xml b/daemon/src/main/res/drawable/ic_notification.xml
deleted file mode 100644
index 5ef738b46..000000000
--- a/daemon/src/main/res/drawable/ic_notification.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
diff --git a/daemon/src/main/res/drawable/ic_statue_monochrome.xml b/daemon/src/main/res/drawable/ic_statue_monochrome.xml
new file mode 100644
index 000000000..b81f9114d
--- /dev/null
+++ b/daemon/src/main/res/drawable/ic_statue_monochrome.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
index 8d8bd1808..b31ced96f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,3 +12,7 @@
# org.gradle.parallel=true
android.useAndroidX=true
+
+# The Compose compiler in :manager needs more than the 512 MB Gradle gives a daemon by
+# default; without this the daemon dies mid-build with "daemon disappeared unexpectedly".
+org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7c4c6ef37..e41b210af 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,16 +7,61 @@ glide = "5.0.9"
okhttp = "5.4.0"
ktfmt = "0.26.0"
coroutines = "1.11.0"
+lifecycle = "2.11.0"
+coil = "3.5.0"
+# Material 3 Expressive is not in any stable material3 release; the expressive
+# APIs graduate through the 1.5.0 alpha line. Pinned explicitly OVER the Compose
+# BOM, which resolves material3 to 1.4.0.
+m3 = "1.5.0-alpha24"
+nav3 = "1.1.4"
+navigationevent = "1.1.2"
+# Governs every androidx.compose.* artifact below; none of them pin a version.
+compose-bom = "2026.06.01"
[plugins]
agp-lib = { id = "com.android.library", version.ref = "agp" }
agp-app = { id = "com.android.application", version.ref = "agp" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
nav-safeargs = { id = "androidx.navigation.safeargs", version.ref = "nav" }
ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" }
lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.4" }
[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.19.0" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
+androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version = "1.13.0" }
+androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.0.1" }
+# Needed to make the in-app browser honour the app's own light/dark choice.
+androidx-webkit = { group = "androidx.webkit", name = "webkit", version = "1.16.0" }
+
+# Compose. The BOM governs every artifact here — do not pin these individually,
+# or BOM alignment is silently defeated.
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "m3" }
+androidx-compose-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "m3" }
+androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
+
+# Navigation 3. Stable since Nov 2025; the back stack is a plain observable list
+# of NavKey objects rather than route strings.
+androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "nav3" }
+androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "nav3" }
+androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycle" }
+androidx-navigationevent-compose = { group = "androidx.navigationevent", name = "navigationevent-compose", version.ref = "navigationevent" }
+
+# GitHub avatars on Home. App icons do NOT go through Coil: they come straight
+# from PackageManager as Drawables via a small LRU cache (see AppIconCache).
+coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
+coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
+
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.11.0" }
+
rikkax-appcompat = { module = "dev.rikka.rikkax.appcompat:appcompat", version = "1.6.1" }
rikkax-core = { module = "dev.rikka.rikkax.core:core", version = "1.4.1" }
rikkax-insets = { module = "dev.rikka.rikkax.insets:insets", version = "1.3.0" }
@@ -56,3 +101,19 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
picocli = { module = "info.picocli:picocli", version = "4.7.7" }
+
+[bundles]
+compose = [
+ "androidx-activity-compose",
+ "androidx-compose-ui",
+ "androidx-compose-ui-graphics",
+ "androidx-compose-ui-tooling-preview",
+ "androidx-compose-material3",
+ "androidx-compose-material3-adaptive-navigation-suite",
+ "androidx-compose-material-icons-extended",
+ "androidx-lifecycle-viewmodel-compose",
+ "androidx-navigation3-runtime",
+ "androidx-navigation3-ui",
+ "androidx-lifecycle-viewmodel-navigation3",
+ "androidx-navigationevent-compose",
+]
diff --git a/manager/README.md b/manager/README.md
new file mode 100644
index 000000000..31926a564
--- /dev/null
+++ b/manager/README.md
@@ -0,0 +1,258 @@
+# Vector Manager Application
+
+## Overview
+
+The manager is the user-facing surface of the Vector framework: a single-activity Jetpack Compose
+application that configures the root daemon over Binder. It is built as `:manager` and replaces the
+legacy `:app` module.
+
+Its architecture is dictated by one constraint. The manager normally runs *parasitically* — the
+Zygisk layer transplants its DEX into a host process, usually `com.android.shell` — so its manifest
+is never registered with the package manager. Nothing that depends on manifest registration exists
+at runtime: no `ContentProvider`, therefore no `androidx.startup`, no `WorkManager`, no guarantee
+that a declared `Application` subclass is ever instantiated, and no resource-backed theme applied by
+the system before the first frame. The same APK is also installable as an ordinary application for
+debugging, and must behave identically in both modes.
+
+## Directory Structure
+
+```text
+src/main/kotlin/org/matrix/vector/manager/
+├── data/
+│ ├── github/ # Activity feed, contributor resolution, canary releases, device-flow auth
+│ ├── log/ # Byte-offset log index and line parser
+│ ├── model/ # Module detection, app and repository models
+│ └── repository/ # Apps, modules, settings, backup, store catalogue, installer
+├── di/ServiceLocator.kt # Hand-rolled service location; no DI framework
+├── ipc/ # DaemonClient (suspending Binder wrapper), package broadcasts
+├── net/ # OkHttp factory and the DNS resolver
+└── ui/
+ ├── components/ # Shared surfaces: panel header, search field, snackbar, ambience
+ ├── navigation/ # Navigation 3 route keys and back stack
+ ├── screens/ # home, modules, logs, repo (store), canary, update, report, web, splash
+ └── theme/ # Seeded colour scheme generation, typography, in-composition locale
+
+src/debug/kotlin/org/matrix/vector/manager/demo/
+ # Scripted device states; compiled into debug builds only
+```
+
+## Process Constraints
+
+* *Service location, not injection.* `ServiceLocator.attach(context)` and `bind(service)` are
+ idempotent and order-independent, because in parasitic mode there is no guaranteed initialisation
+ point and the daemon binder may arrive before or after the first composition.
+* *Theme from code.* The window theme is the platform default; all colour comes from
+ `VectorTheme` at composition time, since a resource theme would require a registered manifest.
+* *Process death is routine.* The host process is killed frequently, so every reading preference
+ (word wrap, header surface, activity window, colour seed) is persisted rather than held in a
+ `ViewModel`.
+* *No `FileProvider`.* Exports go through the Storage Access Framework; the document belongs to
+ DocumentsUI, which is what makes it shareable at all.
+
+## Localisation
+
+The framework's language cannot come from the platform. Per-app language preferences are a manifest
+feature, and this manifest is never registered, so `AppCompatDelegate.setApplicationLocales` has
+nothing to attach to. The chosen language is instead applied inside the composition:
+`ui/theme/AppLocale.kt` provides `LocalConfiguration`, `LocalContext` and `LocalLayoutDirection`
+together, which is what makes a right-to-left language flip the whole app rather than only its text.
+
+Two details are easy to get wrong and were:
+
+* The overridden context must be a `ContextWrapper` around the activity, not the result of
+ `createConfigurationContext` alone. The latter is detached from the activity, so anything reached
+ through `LocalContext` — an activity result launcher, for one — fails to find its owner and the
+ screen crashes on open rather than on language change.
+* Every popup gets its own `AndroidComposeView`, which re-provides the Android composition locals
+ from the base context. Sheets, dialogs and dropdown menus therefore render in the *system*
+ language unless they re-apply the override themselves, which `LocalizedOverlay` exists to do.
+
+Dates and month names are formatted at draw time, never in a model: a name formatted in a repository
+is formatted with `Locale.getDefault()`, which in parasitic mode is the host application's.
+
+## Framework Updates
+
+The daemon gained an install path (`getRootImplementation`, `installFrameworkZip`) because the
+manager cannot run a privileged flash itself. Root is detected by locating the implementation's own binary
+and asking it — `magisk -V`, `ksud`, `apd` — rather than by looking for su, and the flash runs
+through `ProcessBuilder` with an argument list, never a shell string, so a path can never become a
+command. Progress arrives as log lines on an `IFrameworkInstallCallback`, delivered from a thread of the
+daemon's own: a flash takes seconds to minutes, and holding a binder thread for it starves every
+other call the manager is making meanwhile — including the log reads the install screen is doing to
+show what is happening. If the manager goes away mid-flash the install continues, since stopping
+would leave the module tree half-written, and the daemon's own log becomes the only record.
+
+Two builds can share a version code — `git rev-list --count` is identical on a branch and on master
+at the same depth — so a release's `target_commitish` is carried through and compared as well. When
+the codes match and the hashes do not, the update screen says so instead of claiming the device is
+up to date.
+
+Every release publishes a release zip and a debug zip of roughly three times the size. Which one is
+installed is the reader's choice, shown with its size, because the troubleshooting flow elsewhere in
+this app asks people for a debug build.
+
+## Module Updates
+
+Installing a module APK goes through `PackageInstaller` with the download streamed straight into the
+session — no temporary file, and no `FileProvider`, which parasitically does not exist.
+
+The consent story differs sharply between the two modes, which is why the app has a confirmation
+dialog of its own. Inside `com.android.shell` the manager inherits `INSTALL_PACKAGES`, so the commit
+installs a third-party APK with no system prompt whatsoever; standalone, the platform asks as usual.
+In the mode most people run, Vector's dialog is the only consent gate there is, so it names the
+module, the file and its size *before* anything is downloaded.
+
+Whether a module is out of date is one answer shared by three screens — `ServiceLocator.storeEntries`
+joins the catalogue to the installed versions once, and the list's mark, the module's sheet and the
+Store's count all read it. Muting is folded into that answer rather than applied at each reader,
+because a mute only some of them honoured would be worse than none. The two screens that show a
+module *by itself* deliberately ignore it: someone who opened one module's page is asking, not being
+nagged.
+
+Batch updates run one at a time on the application scope. Sequential because four concurrent sessions
+contend for the same disk and, without `INSTALL_PACKAGES`, stack four system dialogs in an order
+nobody chose; on the application scope because four modules take longer than anyone will hold a
+bottom sheet open.
+
+The panel is told when an install lands rather than waiting to overhear it. A replaced package does
+broadcast and the manager does listen, but delivery is the system's business and this process is a
+guest in someone else's; the one install path the app performs itself has no reason to learn about it
+second-hand.
+
+## Demo Mode
+
+Several states worth designing against cannot be produced on a working phone: SELinux policy not
+loaded, the system server not injected, a framework below the API level installed modules need, no
+root implementation at all, every installed module a version behind. `src/debug` contains a scenario list and an `ILSPManagerService` stub
+that scripts the answers it has an opinion about and delegates the rest to the real daemon.
+
+It is a source set rather than a flag. A demo mode that could be switched on in a release build
+would be a way to make the manager report a healthy framework when it is not, which is the one lie
+this app must never be able to tell; a reviewer can confirm by finding no `manager/demo` classes in
+a release APK.
+
+The most useful ones lie about a *version*, because that is what every update decision is made
+against: the framework's own version code comes from the daemon, and so do the installed modules', so
+reporting an old one turns a real release into a real update with nothing else faked — the catalogue,
+the release list, the APK and the install are all genuine. The module scenario stops lying about a
+package the moment that package actually changes, which is what makes it a test of the refresh rather
+than a picture of one.
+
+The scenario host renders `VectorApp()` itself rather than launching the manager activity. Launching
+it lets `ParasiticManagerHooker` hand over the real binder a moment later, which silently undid every
+scenario — including "no daemon at all", which came up reporting a healthy framework.
+
+## Daemon IPC
+
+`DaemonClient` wraps `ILSPManagerService`. Every call suspends on `Dispatchers.IO` and returns a
+`Result`. The binder reference is read *once* per call rather than null-checked and then used,
+which was a time-of-check/time-of-use race, and failures are caught as `Exception` rather than
+`RemoteException` alone — a daemon built without a given method throws `NoSuchMethodError`, which is
+the expected outcome when a newer manager meets an older framework.
+
+Two calls were added to the AIDL for the log reader: `getLogParts(verbose)` lists the rotated parts
+the daemon still holds, and `getLogPart(verbose, name)` opens one. The name arrives from an
+unprivileged process and is used to build a path inside a root-only directory, so it is validated by
+membership in the listing rather than by pattern-matching for traversal sequences.
+
+## Log Reader
+
+The daemon rotates its logcat capture at four megabytes and retains ten parts. A naive reader that
+calls `readLines()` retains several megabytes of `String` per stream inside a process whose heap
+belongs to the host application.
+
+`data/log/LogFile.kt` therefore indexes rather than loads. One sequential byte scan records the
+start offset of every line into a `LongArray`, allocating no `String`. The pane holds a window of at
+most `WINDOW` lines around the viewport and pages outward as the viewport approaches either edge.
+Rows are keyed by absolute line number, which is what allows the window to be extended upwards
+without the viewport lurching: the list re-resolves its first visible item by key after rows are
+inserted above it.
+
+Filtering builds an `IntArray` of matching line numbers and pages through that instead, so a filter
+over a 30,000-line file costs one scan and no re-parse.
+
+## Colour Generation
+
+Android exposes no public API that converts a colour into a Material scheme; `dynamicColorScheme`
+reads the wallpaper and nothing else. `ui/theme/SeedScheme.kt` generates one in *CIE LCh* — the
+same principle as Google's HCT — by holding the seed's hue and chroma and walking L\* across the
+Material tone scale.
+
+The non-obvious part is gamut mapping. Most (lightness, hue) pairs cannot hold the seed's full
+chroma in sRGB, so each tone binary-searches the highest chroma that converts in range. Clamping the
+channels instead shifts hue as tones darken, which is why naive generators drift blue toward purple
+down the ramp. The error ramp is fixed at a red hue regardless of the seed, so destructive actions
+do not change meaning with the theme.
+
+`ui/components/ColorWheel.kt` renders the hue/chroma disc by evaluating every pixel through the same
+conversion, once per tone, off the main thread and cached as an `ImageBitmap`.
+
+## Remote Data
+
+* *Store mirrors.* The full `modules.json` is served by exactly one host today; the public site
+ answers it with 403 and two historical mirrors no longer resolve. Per-module detail *is* served by
+ both, so the mirror lists are deliberately separate — merging them takes the catalogue offline.
+* *Freshness is declared per request.* The OkHttp disk cache is the offline story: on total mirror
+ failure the same request is replayed against the cache alone, so a cold start with no network
+ renders the last known catalogue instead of an error.
+* *DNS-over-HTTPS is a fallback, not a replacement.* `net/VectorDns.kt` attempts DoH, falls
+ through to the system resolver on failure, latches that failure for the session, and disables
+ itself entirely when a proxy is configured. The setting is read per lookup, because OkHttp cannot
+ have its DNS swapped on a live client and rebuilding the shared client would orphan the cache.
+* *Activity feed.* `versionCode` equals `git rev-list --count`, so a commit's distance from HEAD is
+ its version number, and the feed can name exactly which commits an update would bring without an
+ additional endpoint. The total count comes from the `Link: rel="last"` header and the repository
+ statistics from a second request, so both are cached in files of their own — they are answers a
+ cached read cannot reproduce, and writing a failed fetch straight through erased them.
+* *Commit archive.* `/commits` returns at most a hundred per request, so a full history has to be
+ walked backwards and kept. `data/github/CommitArchive.kt` is append-only NDJSON keyed by SHA:
+ everything below the tip is immutable, so a chunk costs its own length rather than a rewrite, and
+ the mutable head window is simply appended again with later lines winning on read.
+
+ The walk is cursored on *date*, not page number — page numbers are relative to the tip and shift
+ under any new commit — and on the **commit** date rather than the author date, because that is
+ what `until` filters on and the two differ on 39 of the newest hundred commits here.
+
+ A date cursor has one failure mode and this repository has it: 100+ commits share
+ `2023-02-26T08:48:49Z`, and asking for commits at or before that second returns the same hundred
+ forever. Inside such a plateau the walk pages by number, which is safe in exactly that position
+ because the window is anchored by an `until` in the past. Completion is an *empty* page and
+ nothing weaker; "nothing new" is what the plateau produces on every request.
+
+ Three pages are fetched per visit and the cursor is left on disk. Sixty requests an hour is the
+ anonymous budget, and a history that assembles over a few sessions is preferable to one that
+ spends all of it on arrival.
+* *Contributor resolution.* GitHub links commits to accounts by email and does not always succeed.
+ A `@users.noreply.github.com` address encodes the account and needs only parsing. Otherwise the
+ name is probed against `/users/{name}` *only if it is shaped like a handle* — containing a digit,
+ hyphen or underscore — because `GET /users/Qing` returns a real and unrelated account, and
+ crediting a contribution to a stranger is worse than leaving it uncredited.
+* *Co-authors.* A `Co-authored-by:` trailer carries an address and no account, and no endpoint turns
+ one into the other — the users search API refuses to index email, and answers `total_count: 0` for
+ a noreply address however it is phrased. But every commit GitHub *has* attributed is a verified
+ email-to-login pair, and the archive is full of them, so trailers are resolved against history
+ already in hand at no request cost. Names are indexed too, one tier weaker and first-wins.
+* *Module detection.* Deciding whether an installed package is a module means opening its APK and
+ its splits as zips: roughly 550 opens on a 363-package device, once paid on every visit to the
+ panel. `data/model/ModuleDetectionCache.kt` keys the answer by package, version code and install
+ time, which is the exact set of things whose change can change the answer.
+
+## Canary Distribution
+
+`actions/artifacts//zip` returns 401 to an anonymous caller; a release asset returns 206. CI
+therefore attaches each master build to a `canary-` prerelease, and the canary screen
+reads `/releases`. No account is required at any point, which matters for users who cannot reach
+GitHub's sign-in at all.
+
+Device-flow sign-in remains available and requests *no scopes*. It only raises the anonymous rate
+limit; if `githubClientId` is not supplied as a Gradle property the app hides sign-in entirely rather
+than presenting a control that cannot work.
+
+## Build Notes
+
+* Material 3 Expressive has not landed in a stable `material3` release, so `material3` is pinned
+ above the Compose BOM rather than resolved from it.
+* Kotlin is declared at the root with `apply false`. AGP 9 otherwise supplies its own, older
+ version, against which Coil's metadata fails to load.
+* `githubClientId` is read from `local.properties` or `~/.gradle/gradle.properties` and defaults to
+ empty.
diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts
new file mode 100644
index 000000000..efd20f903
--- /dev/null
+++ b/manager/build.gradle.kts
@@ -0,0 +1,136 @@
+import java.time.Instant
+
+plugins {
+ alias(libs.plugins.agp.app)
+ // Kotlin itself comes from AGP 9's built-in support — applying
+ // org.jetbrains.kotlin.android is an error since AGP 9.0. Its *version* is taken from
+ // the Kotlin plugin on the buildscript classpath, which the root build pins to the
+ // catalog's version (declared there with `apply false`). That matters here: Coil 3.5
+ // ships class metadata an older compiler refuses to read.
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.ktfmt)
+ alias(libs.plugins.lsplugin.apksign)
+}
+
+ktfmt { kotlinLangStyle() }
+
+kotlin {
+ compilerOptions {
+ // Material 3 Expressive has not landed in a stable material3 release; the
+ // expressive surface is gated behind these annotations even in 1.5.0-alpha24.
+ // Opting in once here beats sprinkling @OptIn through every screen.
+ optIn.addAll(
+ "androidx.compose.material3.ExperimentalMaterial3Api",
+ "androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
+ "androidx.compose.animation.ExperimentalSharedTransitionApi",
+ "androidx.compose.foundation.layout.ExperimentalLayoutApi",
+ )
+ }
+}
+
+// The daemon compiles this module's signing certificate into SignInfo.kt and verifies
+// the manager.apk it serves against it at runtime, so :manager must be signed with the
+// same key as the rest of the module or InstallerVerifier rejects it.
+apksign {
+ storeFileProperty = "androidStoreFile"
+ storePasswordProperty = "androidStorePassword"
+ keyAliasProperty = "androidKeyAlias"
+ keyPasswordProperty = "androidKeyPassword"
+}
+
+val defaultManagerPackageName: String by rootProject.extra
+val injectedPackageName: String by rootProject.extra
+
+android {
+ namespace = defaultManagerPackageName
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+
+ defaultConfig {
+ applicationId = defaultManagerPackageName
+ buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
+ buildConfigField("String", "MANAGER_PACKAGE_NAME", "\"$defaultManagerPackageName\"")
+ buildConfigField("String", "INJECTED_PACKAGE_NAME", "\"$injectedPackageName\"")
+
+ // OAuth client id for the optional GitHub device-flow sign-in on Home. Set
+ // `githubClientId` in local.properties or ~/.gradle/gradle.properties to enable it;
+ // left empty the app hides sign-in entirely rather than offering something broken.
+ val githubClientId = providers.gradleProperty("githubClientId").getOrElse("")
+ buildConfigField("String", "GITHUB_CLIENT_ID", "\"$githubClientId\"")
+
+ // The languages this module is actually translated into, listed from the resource folders
+ // that carry our own strings.xml. AssetManager.getLocales() cannot answer this: it reports
+ // every locale any dependency ships a resource for — AndroidX alone drags in dozens — plus
+ // the pseudo-locales, so a picker built from it offers languages the app has never seen.
+ //
+ // English is added by hand because it is not in a `values-xx` folder to be found: it lives
+ // in `values/`, the base the others fall back to. Scanning alone therefore listed every
+ // language the app has *except* the one it is written in — and a picker without English is
+ // one that someone who switched to Polish to see how it looked cannot use to switch back.
+ val translations =
+ (listOf("en") +
+ file("src/main/res")
+ .listFiles()
+ .orEmpty()
+ .filter { it.isDirectory && it.name.startsWith("values-") }
+ .filter { File(it, "strings.xml").exists() }
+ .map { it.name.removePrefix("values-").replace("-r", "-") })
+ .sorted()
+ buildConfigField("String", "TRANSLATIONS", "\"${translations.joinToString(",")}\"")
+ }
+
+ // ic_launcher.xml references @drawable/ic_statue_monochrome, which lives in the
+ // daemon's resources. Any name collision between the two resource sets becomes a
+ // build error, so keep additions on the daemon side namespaced.
+ sourceSets { getByName("main") { res.srcDir("../daemon/src/main/res") } }
+
+ packaging {
+ resources {
+ excludes += "META-INF/**"
+ excludes += "okhttp3/**"
+ excludes += "kotlin/**"
+ excludes += "**.properties"
+ excludes += "**.bin"
+ }
+ }
+
+ dependenciesInfo.includeInApk = false
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles("proguard-rules.pro")
+ }
+ }
+}
+
+dependencies {
+ implementation(projects.services.managerService)
+
+ implementation(libs.gson)
+ implementation(libs.okhttp)
+ implementation(libs.okhttp.dnsoverhttps)
+ implementation(libs.kotlinx.serialization.json)
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.core.splashscreen)
+ implementation(libs.androidx.webkit)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.kotlinx.coroutines.android)
+
+ // The Compose BOM aligns every androidx.compose.* artifact; none of them is
+ // pinned individually in the version catalog.
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.bundles.compose)
+
+ implementation(libs.coil.compose)
+ implementation(libs.coil.network.okhttp)
+
+ // Tooling dependencies, debug builds only, for UI previews.
+ debugImplementation(libs.androidx.compose.ui.tooling)
+}
diff --git a/manager/proguard-rules.pro b/manager/proguard-rules.pro
new file mode 100644
index 000000000..978023685
--- /dev/null
+++ b/manager/proguard-rules.pro
@@ -0,0 +1,38 @@
+# The zygisk hooker reaches the manager entirely by reflection: it loads
+# ".Constants" out of the injected dex and invokes the static
+# setBinder(IBinder) on it. Neither the class nor the method has a call site inside
+# this APK, so R8 would otherwise remove or rename both and the binder handshake
+# would fail silently at runtime.
+-keep class org.matrix.vector.manager.Constants {
+ public static boolean setBinder(android.os.IBinder);
+}
+
+# ParasiticManagerHooker redirects the resolved activity to this class by name.
+-keep class org.matrix.vector.manager.ui.MainActivity { (); }
+
+# AIDL stubs and the parcelables crossing the daemon boundary.
+-keep class org.lsposed.lspd.** { *; }
+-keep class rikka.parcelablelist.** { *; }
+
+# kotlinx.serialization keeps generated serializers reachable from the companion.
+-keepclassmembers class **$$serializer { *** descriptor; }
+-keepclasseswithmembers class ** {
+ kotlinx.serialization.KSerializer serializer(...);
+}
+
+# Gson models are constructed reflectively from field names.
+-keepclassmembers class org.matrix.vector.manager.data.model.** { ; }
+
+# OkHttp / Okio ship analysis-only references to optional platform classes.
+-dontwarn okhttp3.internal.**
+-dontwarn org.conscrypt.**
+-dontwarn org.bouncycastle.**
+-dontwarn org.openjsse.**
+
+# androidx.window compiles against the OEM window extensions and the older sidecar
+# interface. Neither ships in the SDK — they are provided by the device at runtime, and
+# on a device that has neither the library falls back — so R8 sees the references as
+# unresolvable and refuses to complete. The navigation suite scaffold pulls the library
+# in, so the manager inherits them whether or not it ever asks about a folding screen.
+-dontwarn androidx.window.extensions.**
+-dontwarn androidx.window.sidecar.**
diff --git a/manager/src/debug/AndroidManifest.xml b/manager/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..65019ecde
--- /dev/null
+++ b/manager/src/debug/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt
new file mode 100644
index 000000000..a0281c434
--- /dev/null
+++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoActivity.kt
@@ -0,0 +1,158 @@
+package org.matrix.vector.manager.demo
+
+import org.lsposed.lspd.ILSPManagerService
+import kotlinx.coroutines.launch
+import androidx.lifecycle.lifecycleScope
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import org.matrix.vector.manager.di.ServiceLocator
+import org.matrix.vector.manager.ui.VectorApp
+import org.matrix.vector.manager.ui.theme.LocalizedContent
+import org.matrix.vector.manager.ui.theme.VectorTheme
+
+/**
+ * The way in to the scripted states, and the reason this whole thing is a separate source set.
+ *
+ * It exists only in `src/debug`, so a release build does not merely branch around it — there is no
+ * such class to compile and no such activity in the merged manifest. That distinction matters more
+ * here than in an ordinary app: a demo mode that could be switched on in a release build would be a
+ * way to make the manager report the framework as healthy when it is not, which is the one lie this
+ * app must never be able to tell. A reviewer can confirm it by looking for
+ * `org.matrix.vector.manager.demo` in a release APK's classes and finding nothing.
+ *
+ * It hosts the app itself rather than launching MainActivity, and that is not a stylistic choice.
+ * The first version did launch it, and every scenario silently did nothing: `ParasiticManagerHooker`
+ * intercepts the manager activity starting and hands the *real* binder to `Constants.setBinder`,
+ * overwriting whatever was bound a moment earlier. Even "no daemon at all" came up reporting a
+ * healthy framework — the failure mode a test harness can least afford, since it looks like a pass.
+ * Rendering VectorApp here means no manager activity is ever launched, so nothing re-binds behind
+ * us.
+ */
+class DemoActivity : ComponentActivity() {
+
+ /**
+ * Captured before anything is bound, so a scenario can delegate what it does not script.
+ *
+ * Frequently null: the hooker sends the binder when the app's class loader is first asked for,
+ * which happens after this activity is constructed. That is fine — a scenario with no real
+ * daemon behind it simply has empty lists, and the scripted answers are the point.
+ */
+ private val realService = ServiceLocator.service.value
+
+ /**
+ * What the demo insists the binder is, and whether it is currently insisting.
+ *
+ * `ParasiticManagerHooker` hands the real binder to `Constants.setBinder` when the manager's
+ * class loader is first obtained — which is *after* a scenario has been chosen, so a single
+ * bind was quietly undone a moment later and every scenario reported a healthy framework. This
+ * re-asserts the choice whenever something else replaces it. It settles immediately: the next
+ * emission is the pinned value, which the collector then ignores.
+ */
+ private var pinned: ILSPManagerService? = null
+
+ private var pinning = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ ServiceLocator.attach(this)
+
+ lifecycleScope.launch {
+ ServiceLocator.service.collect { current ->
+ if (pinning && current !== pinned) ServiceLocator.bind(pinned)
+ }
+ }
+
+ setContent {
+ var scenario by remember { mutableStateOf(null) }
+
+ if (scenario == null) {
+ VectorTheme { ScenarioList { picked -> scenario = install(picked) } }
+ } else {
+ // Back returns to the picker rather than leaving, so trying six states in a row is
+ // not six trips through the launcher. The real binder goes back on the way out, so
+ // the app is never left holding a fake after the demo is done with it.
+ BackHandler {
+ // Stop insisting first, or the collector would immediately undo this.
+ pinning = false
+ ServiceLocator.bind(realService)
+ scenario = null
+ }
+ LocalizedContent { VectorTheme { VectorApp() } }
+ }
+ }
+ }
+
+ private fun install(scenario: DemoScenario): DemoScenario {
+ if (scenario.id == "healthy") {
+ // Deliberately does not bind: realService is usually null here, and forcing that would
+ // break the app rather than restore it. Letting go is enough — whatever the hooker
+ // bound is the real thing.
+ pinning = false
+ pinned = null
+ return scenario
+ }
+ pinned =
+ if (!scenario.connected) null else FakeManagerService(scenario, realService)
+ pinning = true
+ ServiceLocator.bind(pinned)
+ return scenario
+ }
+}
+
+@Composable
+private fun ScenarioList(onPick: (DemoScenario) -> Unit) {
+ Scaffold(modifier = Modifier.fillMaxSize()) { padding ->
+ LazyColumn(modifier = Modifier.padding(padding)) {
+ item {
+ Column(Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp)) {
+ Text(
+ "Demo states",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ "Device states that cannot be reached without breaking the phone. " +
+ "The scripted answers stop at the binder; everything above it is " +
+ "real. Back returns here.",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ HorizontalDivider()
+ }
+ items(DEMO_SCENARIOS, key = { it.id }) { scenario ->
+ ListItem(
+ modifier = Modifier.clickable { onPick(scenario) },
+ headlineContent = { Text(scenario.title) },
+ supportingContent = { Text(scenario.summary) },
+ )
+ }
+ }
+ }
+}
diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoScenario.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoScenario.kt
new file mode 100644
index 000000000..7ee890d63
--- /dev/null
+++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/DemoScenario.kt
@@ -0,0 +1,212 @@
+package org.matrix.vector.manager.demo
+
+import org.lsposed.lspd.ILSPManagerService
+
+/**
+ * A device state the manager cannot otherwise be shown.
+ *
+ * Only states that need a *broken or unusual system* are here. Anything reachable by using the app
+ * normally — an empty search, airplane mode, a module with nothing selected — is deliberately
+ * absent: those get found the first day a build is in anyone's hands, and scripting them would be
+ * upkeep with no return.
+ *
+ * Everything downstream of the binder runs for real against these values: the status derivation,
+ * the issue list, the update logic, the install screen. This scripts what the *device* says, not
+ * what the UI shows, so a bug in how the manager reacts is still a bug you can see here.
+ */
+data class DemoScenario(
+ val id: String,
+ val title: String,
+ val summary: String,
+
+ /** False binds nothing at all, which is how the framework reads as not activated. */
+ val connected: Boolean = true,
+
+ /** Delay on every status call. Non-zero is the only way to hold "Checking…" still. */
+ val stallMillis: Long = 0,
+ val sepolicyLoaded: Boolean = true,
+ val systemServerRequested: Boolean = true,
+ val dex2oatFlagsLoaded: Boolean = true,
+ val dex2oatCompatibility: Int = ILSPManagerService.DEX2OAT_OK,
+
+ /**
+ * What the framework claims to implement.
+ *
+ * Lowering it is how a module becomes incompatible without fabricating a module: the real ones
+ * on the device declare a real minimum, and the framework simply stops meeting it.
+ */
+ val xposedApiVersion: Int = -1,
+ val xposedVersionCode: Long = -1,
+ val rootImplementation: Int = ILSPManagerService.ROOT_MAGISK,
+ val rootVersion: String? = "28.1",
+ val install: InstallScript = InstallScript.SUCCEEDS,
+
+ /**
+ * What version the device claims its installed modules are.
+ *
+ * The same trick the framework update uses, one level down: whether a module is out of date is
+ * decided by comparing the store catalogue against the version the *daemon* reports, so
+ * reporting an old one turns every module the catalogue knows into an update. The catalogue,
+ * the releases and the APKs are all genuinely the store's — the only lie is the number this
+ * device claims to be on, which is the one thing that cannot be arranged without keeping a
+ * stack of outdated module APKs around to install.
+ */
+ val moduleVersions: ModuleVersionScript = ModuleVersionScript.REAL,
+) {
+
+ /** Whether installed module versions are passed through or rewritten. */
+ enum class ModuleVersionScript {
+ REAL,
+ OUTDATED,
+ }
+
+ /**
+ * How a flash behaves when the install screen asks for one.
+ *
+ * Reachable through this seam after all, which was not obvious: whether an update *exists* is
+ * decided by comparing the release list against the installed version code — and that version
+ * comes from the daemon, not from GitHub. Reporting an old one is enough to make a real
+ * release look like an update, so the whole flow can be exercised without faking any network
+ * traffic. The release list itself is genuinely GitHub's, which makes this closer to the real
+ * thing than a canned one would be.
+ */
+ enum class InstallScript {
+ SUCCEEDS,
+
+ /**
+ * Fails after output has already been streamed.
+ *
+ * The one worth having: a flash that fails *before* it starts is a message, but a flash
+ * that dies halfway leaves the module tree in whatever state the installer reached, and
+ * that is the case the screen has to report usefully.
+ */
+ FAILS_PARTWAY,
+
+ /** Refused outright, with no output. */
+ NO_ROOT,
+ }
+
+ companion object {
+ /** -1 means "whatever the real daemon says", so a scenario only lies where it means to. */
+ const val PASS_THROUGH = -1
+ }
+}
+
+/**
+ * The menu.
+ *
+ * Ordered by how hard the state is to reach honestly, not by severity: the ones at the top cannot
+ * be produced on a working phone at all.
+ */
+val DEMO_SCENARIOS: List =
+ listOf(
+ DemoScenario(
+ id = "healthy",
+ title = "Healthy",
+ summary = "Pass everything through to the real daemon. The way back.",
+ ),
+ DemoScenario(
+ id = "sepolicy",
+ title = "SELinux policy not loaded",
+ summary = "Degraded, one cause. Needs a root implementation that skipped our rules.",
+ sepolicyLoaded = false,
+ ),
+ DemoScenario(
+ id = "system-server",
+ title = "System framework injection failed",
+ summary = "Degraded, one cause. Normally needs another root module interfering.",
+ systemServerRequested = false,
+ ),
+ DemoScenario(
+ id = "dex2oat",
+ title = "Dex optimizer wrapper unavailable",
+ summary = "Degraded, one cause. Needs system properties removed or changed.",
+ dex2oatFlagsLoaded = false,
+ dex2oatCompatibility = ILSPManagerService.DEX2OAT_MOUNT_FAILED,
+ ),
+ DemoScenario(
+ id = "all-issues",
+ title = "All three causes at once",
+ summary = "Whether the issue list reads as a list or as a wall.",
+ sepolicyLoaded = false,
+ systemServerRequested = false,
+ dex2oatFlagsLoaded = false,
+ dex2oatCompatibility = ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT,
+ ),
+ DemoScenario(
+ id = "inactive",
+ title = "Framework not activated",
+ summary = "No daemon at all. Every screen that needs one has to say so.",
+ connected = false,
+ ),
+ DemoScenario(
+ id = "checking",
+ title = "Checking, held still",
+ summary = "The transient state on arrival, stalled for eight seconds.",
+ stallMillis = 8_000,
+ ),
+ DemoScenario(
+ id = "api-too-old",
+ title = "Framework below what modules need",
+ summary = "API 82. Installed modules that need more become incompatible.",
+ xposedApiVersion = 82,
+ ),
+ DemoScenario(
+ id = "root-none",
+ title = "No root implementation",
+ summary = "Nothing to flash through. The install path must refuse, not fail.",
+ rootImplementation = ILSPManagerService.ROOT_NONE,
+ rootVersion = null,
+ install = DemoScenario.InstallScript.NO_ROOT,
+ ),
+ DemoScenario(
+ id = "root-multiple",
+ title = "Two root implementations fighting",
+ summary = "Flashing through either would be a guess, and must be named as such.",
+ rootImplementation = ILSPManagerService.ROOT_MULTIPLE,
+ rootVersion = null,
+ install = DemoScenario.InstallScript.NO_ROOT,
+ ),
+ DemoScenario(
+ id = "root-too-old",
+ title = "Root implementation too old",
+ summary = "Installed but not usable. Distinct from having none.",
+ rootImplementation = ILSPManagerService.ROOT_TOO_OLD,
+ rootVersion = "20.4",
+ install = DemoScenario.InstallScript.NO_ROOT,
+ ),
+ DemoScenario(
+ id = "root-ksu",
+ title = "KernelSU",
+ summary = "The install path quotes the implementation it found.",
+ rootImplementation = ILSPManagerService.ROOT_KERNELSU,
+ rootVersion = "12045",
+ ),
+ DemoScenario(
+ id = "root-apatch",
+ title = "APatch",
+ summary = "As above, third implementation.",
+ rootImplementation = ILSPManagerService.ROOT_APATCH,
+ rootVersion = "10763",
+ ),
+ DemoScenario(
+ id = "update-available",
+ title = "An update is available",
+ summary = "Reports version 1, so a real release becomes an update. Shows the picker.",
+ xposedVersionCode = 1,
+ ),
+ DemoScenario(
+ id = "install-fails",
+ title = "Flash that dies halfway",
+ summary = "Output already streamed, then a non-zero exit. The case that bites.",
+ xposedVersionCode = 1,
+ install = DemoScenario.InstallScript.FAILS_PARTWAY,
+ ),
+ DemoScenario(
+ id = "modules-outdated",
+ title = "Every module is out of date",
+ summary =
+ "Reports old versions, so the store is ahead of all of them. Installs are real.",
+ moduleVersions = DemoScenario.ModuleVersionScript.OUTDATED,
+ ),
+ )
diff --git a/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt
new file mode 100644
index 000000000..724db108d
--- /dev/null
+++ b/manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt
@@ -0,0 +1,271 @@
+package org.matrix.vector.manager.demo
+
+import android.content.Intent
+import android.content.pm.PackageInfo
+import android.content.pm.ResolveInfo
+import android.os.ParcelFileDescriptor
+import org.lsposed.lspd.IFrameworkInstallCallback
+import org.lsposed.lspd.ILSPManagerService
+import org.lsposed.lspd.models.Application
+import org.lsposed.lspd.models.UserInfo
+import rikka.parcelablelist.ParcelableListSlice
+
+/**
+ * The daemon, as a script.
+ *
+ * The fake sits at the *binder*, which is the boundary between this app and the privileged system,
+ * and is the reason a demo mode is worth having at all: everything from here inwards — DaemonClient,
+ * the repositories, the view models, the status derivation that turns three booleans into an issue
+ * list — runs exactly as it does in production. What is faked is what the device says about itself,
+ * not what the manager concludes. A bug in the concluding is still visible.
+ *
+ * Two other seams were considered and rejected. Faking a repository would have meant the status
+ * derivation never ran, which is the code most likely to be wrong. Faking a view model would have
+ * meant testing the screen against a pipeline that was not there — and the bugs this week lived in
+ * the pipeline, not the screen.
+ *
+ * Anything the scenario does not have an opinion about is delegated to the real daemon when one is
+ * connected, so the module list, the app list and the logs stay real while the framework's health
+ * is a lie. With no daemon present the delegating calls return empties rather than throwing, so the
+ * demo is still usable on a device with no Vector installed.
+ *
+ * Subclassing `Stub()` rather than proxying the interface is deliberate on both counts: DaemonClient
+ * checks `asBinder().isBinderAlive`, which only a real Binder answers — and a new AIDL method breaks
+ * this file's compilation, which is the point. A fake that silently kept working while the daemon
+ * grew a new question would quietly stop covering it.
+ */
+class FakeManagerService(
+ private val scenario: DemoScenario,
+ private val real: ILSPManagerService?,
+) : ILSPManagerService.Stub() {
+
+ /**
+ * What each package's version was when the scenario started.
+ *
+ * The scenario claims everything is out of date, and something has to decide when to stop
+ * claiming it. Recording the first answer per package and comparing against it means the lie
+ * ends exactly when the package actually changes — so an install performed by the manager is
+ * visible in the manager, which is the behaviour worth testing here.
+ */
+ private val baselineVersions = java.util.concurrent.ConcurrentHashMap()
+
+ private fun stall() {
+ if (scenario.stallMillis > 0) Thread.sleep(scenario.stallMillis)
+ }
+
+ // ---- what the scenario exists to lie about ------------------------------------------------
+
+ override fun isSepolicyLoaded(): Boolean {
+ stall()
+ return scenario.sepolicyLoaded
+ }
+
+ override fun systemServerRequested(): Boolean {
+ stall()
+ return scenario.systemServerRequested
+ }
+
+ override fun dex2oatFlagsLoaded(): Boolean {
+ stall()
+ return scenario.dex2oatFlagsLoaded
+ }
+
+ override fun getDex2OatWrapperCompatibility(): Int = scenario.dex2oatCompatibility
+
+ override fun getXposedApiVersion(): Int =
+ scenario.xposedApiVersion.takeIf { it != DemoScenario.PASS_THROUGH }
+ ?: real?.xposedApiVersion
+ ?: 0
+
+ override fun getXposedVersionCode(): Long =
+ scenario.xposedVersionCode.takeIf { it != DemoScenario.PASS_THROUGH.toLong() }
+ ?: real?.xposedVersionCode
+ ?: 0L
+
+ override fun getRootImplementation(): Int = scenario.rootImplementation
+
+ /**
+ * Passed through, because a scenario that lied about the commit would be testing the *mismatch*
+ * warning rather than the states this harness exists for. Add a field here when there is a
+ * scenario that needs one.
+ */
+ override fun getFrameworkCommit(): String? = real?.frameworkCommit
+
+ override fun getRootImplementationVersion(): String? = scenario.rootVersion
+
+ /**
+ * A flash, without a flash.
+ *
+ * Emits on its own thread and never blocks the caller, because the real one does not either —
+ * a screen that only works when the lines arrive on the binder thread would pass here and hang
+ * on a device.
+ */
+ override fun installFrameworkZip(zipPath: String?, callback: IFrameworkInstallCallback?) {
+ if (callback == null) return
+ Thread {
+ fun say(line: String) {
+ runCatching { callback.onLine(line) }
+ Thread.sleep(220)
+ }
+ when (scenario.install) {
+ DemoScenario.InstallScript.NO_ROOT -> {
+ runCatching { callback.onFinished(ILSPManagerService.INSTALL_NO_ROOT) }
+ }
+ DemoScenario.InstallScript.SUCCEEDS -> {
+ say("- Target: $zipPath")
+ say("- Extracting module files")
+ say("- Device is arm64-v8a API 36")
+ say("- Installing Vector")
+ say("- Setting permissions")
+ say("- Done. Reboot to apply.")
+ runCatching { callback.onFinished(0) }
+ }
+ DemoScenario.InstallScript.FAILS_PARTWAY -> {
+ say("- Target: $zipPath")
+ say("- Extracting module files")
+ say("- Device is arm64-v8a API 36")
+ say("- Installing Vector")
+ say("! Failed to copy zygisk binary: No space left on device")
+ runCatching { callback.onFinished(1) }
+ }
+ }
+ }
+ .start()
+ }
+
+ // ---- everything else is the real device, when there is one ---------------------------------
+
+ /**
+ * The installed package list, optionally rewritten to look old.
+ *
+ * This one call is where "is there an update for this module" is really decided: the catalogue
+ * says what the newest version is, and the comparison is against what this returns. Reporting
+ * a low version here is therefore the whole of the "modules are out of date" scenario, and it
+ * has the property that makes these scenarios worth having — nothing downstream is faked. The
+ * catalogue is the real one, the releases are real, the APK that gets installed is real, and so
+ * is the install.
+ *
+ * The rewrite is applied to every package rather than to modules alone, because telling them
+ * apart means opening APKs and this is the daemon's side of the wire, where that answer is not
+ * known. The visible cost is that the demo's module rows all read `0.1-demo` — which is the
+ * signal that the scenario is on, and no worse than the arbitrary number it replaces.
+ */
+ override fun getInstalledPackagesFromAllUsers(
+ flags: Int,
+ filterNoProcess: Boolean,
+ ): ParcelableListSlice {
+ val actual =
+ real?.getInstalledPackagesFromAllUsers(flags, filterNoProcess)
+ ?: return ParcelableListSlice(emptyList())
+ if (scenario.moduleVersions == DemoScenario.ModuleVersionScript.REAL) return actual
+ // Mutated in place: these are already unparcelled copies belonging to this process, not the
+ // daemon's own objects.
+ val rewritten =
+ actual.list.map { info ->
+ val baseline = baselineVersions.putIfAbsent(info.packageName, info.longVersionCode)
+ if (baseline != null && baseline != info.longVersionCode) {
+ // This one has genuinely changed under us since the scenario started, which
+ // for a demo means the manager just installed it. Reporting the truth from
+ // here is what makes this a test rather than a picture: the row has to stop
+ // being out of date, the count has to drop, and the panel has to notice
+ // without being left and re-entered. Keep lying and the install always looks
+ // like it did nothing.
+ return@map info
+ }
+ info.also {
+ it.longVersionCode = 1
+ it.versionName = "0.1-demo"
+ }
+ }
+ return ParcelableListSlice(rewritten)
+ }
+
+ override fun enabledModules(): Array = real?.enabledModules() ?: emptyArray()
+
+ override fun enableModule(packageName: String?): Boolean =
+ real?.enableModule(packageName) ?: false
+
+ override fun disableModule(packageName: String?): Boolean =
+ real?.disableModule(packageName) ?: false
+
+ override fun setModuleScope(packageName: String?, scope: MutableList?): Boolean =
+ real?.setModuleScope(packageName, scope) ?: false
+
+ override fun getModuleScope(packageName: String?): MutableList =
+ real?.getModuleScope(packageName) ?: mutableListOf()
+
+ override fun isVerboseLog(): Boolean = real?.isVerboseLog ?: false
+
+ override fun setVerboseLog(enabled: Boolean) {
+ real?.setVerboseLog(enabled)
+ }
+
+ override fun getVerboseLog(): ParcelFileDescriptor? = real?.verboseLog
+
+ override fun getModulesLog(): ParcelFileDescriptor? = real?.modulesLog
+
+ override fun getLogParts(verbose: Boolean): MutableList =
+ real?.getLogParts(verbose) ?: mutableListOf()
+
+ override fun getLogPart(verbose: Boolean, name: String?): ParcelFileDescriptor? =
+ real?.getLogPart(verbose, name)
+
+ override fun getXposedVersionName(): String? = real?.xposedVersionName
+
+ override fun clearLogs(verbose: Boolean): Boolean = real?.clearLogs(verbose) ?: false
+
+ override fun getPackageInfo(packageName: String?, flags: Int, uid: Int): PackageInfo? =
+ real?.getPackageInfo(packageName, flags, uid)
+
+ override fun forceStopPackage(packageName: String?, userId: Int) {
+ real?.forceStopPackage(packageName, userId)
+ }
+
+ /** Not delegated. A demo build must not be able to reboot the device by accident. */
+ override fun reboot() = Unit
+
+ override fun uninstallPackage(packageName: String?, userId: Int): Boolean =
+ real?.uninstallPackage(packageName, userId) ?: false
+
+ override fun getUsers(): MutableList = real?.users ?: mutableListOf()
+
+ override fun installExistingPackageAsUser(packageName: String?, userId: Int): Int =
+ real?.installExistingPackageAsUser(packageName, userId) ?: 0
+
+ override fun startActivityAsUserWithFeature(intent: Intent?, userId: Int): Int =
+ real?.startActivityAsUserWithFeature(intent, userId) ?: 0
+
+ override fun queryIntentActivitiesAsUser(
+ intent: Intent?,
+ flags: Int,
+ userId: Int,
+ ): ParcelableListSlice =
+ real?.queryIntentActivitiesAsUser(intent, flags, userId) ?: ParcelableListSlice(emptyList())
+
+ override fun setHiddenIcon(hide: Boolean) {
+ real?.setHiddenIcon(hide)
+ }
+
+ override fun getLogs(zipFd: ParcelFileDescriptor?) {
+ real?.getLogs(zipFd)
+ }
+
+ override fun restartFor(intent: Intent?) {
+ real?.restartFor(intent)
+ }
+
+ override fun optimizePackage(packageName: String?): Boolean =
+ real?.optimizePackage(packageName) ?: false
+
+ override fun enableStatusNotification(): Boolean = real?.enableStatusNotification() ?: false
+
+ override fun setEnableStatusNotification(enable: Boolean) {
+ real?.setEnableStatusNotification(enable)
+ }
+
+ override fun getAutoInclude(packageName: String?): Boolean =
+ real?.getAutoInclude(packageName) ?: false
+
+ override fun setAutoInclude(packageName: String?, enable: Boolean): Boolean =
+ real?.setAutoInclude(packageName, enable) ?: false
+}
diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..08a9c25a2
--- /dev/null
+++ b/manager/src/main/AndroidManifest.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt b/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt
new file mode 100644
index 000000000..0f6c2168b
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/Constants.kt
@@ -0,0 +1,34 @@
+package org.matrix.vector.manager
+
+import android.os.IBinder
+import kotlin.system.exitProcess
+import org.lsposed.lspd.ILSPManagerService
+import org.matrix.vector.manager.di.ServiceLocator
+
+/**
+ * The one entry point the framework reaches by reflection.
+ *
+ * `ParasiticManagerHooker.sendBinderToManager` loads `.Constants` out of the
+ * injected dex and invokes the static `setBinder(IBinder)` below. Nothing inside this APK calls it,
+ * so R8 must be told to keep both — see `proguard-rules.pro`. Renaming this class or the method
+ * breaks the handshake silently, at runtime, with no compile error anywhere.
+ */
+object Constants {
+ const val TAG = "VectorManager"
+
+ @JvmStatic
+ fun setBinder(binder: IBinder): Boolean {
+ ServiceLocator.bind(ILSPManagerService.Stub.asInterface(binder))
+
+ try {
+ // If the daemon dies the manager is holding a dead binder and every screen would
+ // silently show empty state, which reads as "you have no modules" rather than "the
+ // framework is gone". Exiting is blunt but honest.
+ binder.linkToDeath({ exitProcess(0) }, 0)
+ } catch (_: Exception) {
+ exitProcess(0)
+ }
+
+ return binder.isBinderAlive
+ }
+}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/github/CommitArchive.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/CommitArchive.kt
new file mode 100644
index 000000000..67c984717
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/CommitArchive.kt
@@ -0,0 +1,157 @@
+package org.matrix.vector.manager.data.github
+
+import java.io.File
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+/**
+ * Every commit the app has ever seen, kept on disk.
+ *
+ * The activity feed used to be one request — the newest hundred commits within a date window — and
+ * "from the start of the project" cannot be answered that way: `/commits` returns at most a hundred
+ * per request and there is no widening the window past that. The history has to be walked backwards
+ * a page at a time and remembered, which is what this is.
+ *
+ * ## Why an append-only file
+ *
+ * A commit that is not at the tip never changes. Its message, its author and its date are fixed the
+ * moment it is buried under another commit, so the overwhelming majority of this archive is
+ * immutable and rewriting it would be pure waste. New pages are *appended*, one JSON object per
+ * line, which costs the length of the chunk rather than the length of the history — the difference
+ * between writing 20 KB and rewriting 600 KB, every time, on a phone.
+ *
+ * The head is the exception. The newest commits can still be amended, rebased or force-pushed away,
+ * so the refresh rewrites that window on every run. Appending a second copy of a commit is
+ * therefore normal rather than a bug, and [read] resolves it: later lines win, so the freshest
+ * record of any SHA is the one that survives. The file is compacted when the duplicates outgrow the
+ * real content, which for a normal refresh rhythm is rarely.
+ *
+ * ## Why SHA is the key
+ *
+ * It is the only identifier git guarantees. Dates collide — several commits share a second, and the
+ * page boundary is *chosen* by date, so the same commit legitimately arrives twice — and positions
+ * shift as new work lands. Keying on the SHA makes an overlapping page harmless, which in turn lets
+ * the backfill cursor be a date rather than a page number. That matters: page numbers are
+ * invalidated by every new commit, dates are not.
+ */
+class CommitArchive(private val file: File, private val stateFile: File, private val json: Json) {
+
+ /**
+ * Where the backwards walk has reached.
+ *
+ * [oldestSeenEpochSeconds] is the cursor — the next request asks for commits at or before it —
+ * and [complete] records that a request came back empty, which is the only signal that the
+ * history has genuinely run out. All of it is persisted because the walk is spread across
+ * sessions: an anonymous client gets sixty requests an hour, and a few thousand commits is
+ * thirty of them, so finishing in one sitting is neither possible nor polite.
+ *
+ * [pageWithinCursor] is what makes the walk correct on a repository that has been squashed or
+ * imported. This one has 100+ commits stamped with the same second — `2023-02-26T08:48:49Z` —
+ * and a cursor alone cannot get past them: asking for commits at or before that second returns
+ * the same hundred every time, and the walk stalls a fifth of the way through history while
+ * looking exactly like success. Inside such a plateau the walk pages by number instead, which
+ * is safe here in a way it is not in general: the page window is anchored by `until` at a
+ * moment in the past, so new commits land outside it and cannot shift it.
+ */
+ @Serializable
+ data class State(
+ val oldestSeenEpochSeconds: Long = 0,
+ val complete: Boolean = false,
+ /** Purely for the log line that explains a slow or stalled backfill. */
+ val pagesFetched: Int = 0,
+ /** Which page of the current cursor's timestamp to ask for next; 1 unless in a plateau. */
+ val pageWithinCursor: Int = 1,
+ /**
+ * The walk that wrote this file.
+ *
+ * A cursor left behind by an older, wronger walk is worse than no cursor at all — the one
+ * that stalled on the plateau above saved `complete: true`, and honouring that would mean
+ * never looking again. Anything not written by the current walk is re-walked from the top.
+ */
+ val algorithm: Int = 0,
+ )
+
+ companion object {
+ /** Bump when the walk changes in a way that invalidates a saved cursor. */
+ const val ALGORITHM = 1
+ }
+
+ fun state(): State =
+ runCatching { json.decodeFromString(stateFile.readText()) }
+ .getOrDefault(State())
+ .let { if (it.algorithm == ALGORITHM) it else State(algorithm = ALGORITHM) }
+
+ fun writeState(state: State) {
+ runCatching { stateFile.writeText(json.encodeToString(state.copy(algorithm = ALGORITHM))) }
+ }
+
+ /**
+ * Everything held, newest first, one record per SHA.
+ *
+ * Later lines win, which is what makes appending a rewritten head window correct rather than
+ * corrupting: the newest copy of a commit is the one that ends up furthest down the file.
+ */
+ fun read(): List {
+ parsed?.let {
+ return it
+ }
+ return parse().also { parsed = it }
+ }
+
+ /**
+ * The parsed file, held for as long as it is unchanged.
+ *
+ * One load reads this twice — once to lay out the feed, once to know what the backfill already
+ * has — and a backfill reads it again after appending. At three thousand commits that is three
+ * passes over four megabytes of JSON per visit to the foot of the list, all of it to reproduce
+ * a list that has not changed. Every writer here invalidates it, so there is no way to hold a
+ * stale copy.
+ */
+ @Volatile private var parsed: List? = null
+
+ private fun parse(): List {
+ if (!file.isFile) return emptyList()
+ val byShaLatestWins = LinkedHashMap()
+ runCatching {
+ file.forEachLine { line ->
+ if (line.isBlank()) return@forEachLine
+ runCatching { json.decodeFromString(line) }
+ .getOrNull()
+ // A truncated final line — a process killed mid-append — costs that one commit
+ // and nothing else. It is why this is a line format and not one JSON document.
+ ?.let { byShaLatestWins[it.sha] = it }
+ }
+ }
+ return byShaLatestWins.values.sortedByDescending { it.commit.author.date }
+ }
+
+ /** Appends a chunk. Duplicates are expected and are resolved on read, not here. */
+ fun append(commits: List) {
+ if (commits.isEmpty()) return
+ parsed = null
+ runCatching {
+ file.parentFile?.mkdirs()
+ file.appendText(commits.joinToString("\n", postfix = "\n") { json.encodeToString(it) })
+ }
+ }
+
+ /**
+ * Rewrites the file with one line per commit, dropping the superseded copies.
+ *
+ * Only worth doing when the duplicates have grown past the content itself, which takes a great
+ * many refreshes — the head window is a hundred commits and a full history is thousands.
+ */
+ fun compactIfWasteful() {
+ if (!file.isFile) return
+ val lines = runCatching { file.readLines().count { it.isNotBlank() } }.getOrDefault(0)
+ val unique = read()
+ if (lines <= unique.size * 2) return
+ runCatching {
+ val tmp = File(file.parentFile, file.name + ".tmp")
+ tmp.writeText(unique.joinToString("\n", postfix = "\n") { json.encodeToString(it) })
+ tmp.renameTo(file)
+ parsed = unique
+ }
+ }
+}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/github/FeedLayout.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/FeedLayout.kt
new file mode 100644
index 000000000..345534734
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/FeedLayout.kt
@@ -0,0 +1,202 @@
+package org.matrix.vector.manager.data.github
+
+import java.util.Calendar
+import java.util.Locale
+import kotlin.math.sqrt
+
+/**
+ * What the rail draws, in order.
+ *
+ * The rail is deliberately **not** a branch graph. This project squash-merges, so its history is
+ * linear by construction — there were zero merge commits in the last 44 — and drawing lanes would
+ * be inventing structure that does not exist. What the data *does* carry, and a uniform list
+ * throws away, is time and the reader's own position in it. That is what these items encode.
+ */
+sealed interface FeedItem {
+
+ /** A commit. The rail spans its full height; elapsed time is carried by [Gap] below it. */
+ data class Commit(
+ val commit: TimelineCommit,
+ val isFirst: Boolean,
+ val isLast: Boolean,
+ ) : FeedItem
+
+ /**
+ * The elapsed time between two commits, as rail.
+ *
+ * A separate row rather than a trailing segment inside the commit row, because a commit row's
+ * height is set by its text and would otherwise leave the line stopping short of the next
+ * node — the rail visibly broke between same-day commits.
+ */
+ data class Gap(val days: Int, val afterSha: String) : FeedItem
+
+ /**
+ * The line between what the reader is running and what they are not.
+ *
+ * `versionCode` is `git rev-list --count`, so a commit's distance from HEAD is exactly its
+ * version number — no guessing and no extra endpoint. Everything above this line is what an
+ * update would actually bring, named commit by commit, which is the question a framework
+ * user actually has.
+ */
+ data class InstalledMarker(
+ val versionCode: Long,
+ val commitsAhead: Int,
+ /**
+ * True when the installed build is *past* the head of master.
+ *
+ * That cannot happen to anyone running a published build, so it means the framework was
+ * built locally or from another branch. Worth saying: the feed below is then not the
+ * history of what is installed, and neither an issue report nor a bisect against it means
+ * what the reader would assume.
+ */
+ val aheadOfMaster: Boolean = false,
+ ) : FeedItem
+
+ /**
+ * Where the rail crosses into an earlier month, with what that month amounted to.
+ *
+ * A bare month name is just a scroll landmark. With the month's own totals it becomes the
+ * summary layer of the timeline: you can read the project's shape by skimming the separators
+ * without reading a single commit.
+ */
+ data class MonthMarker(
+ /** Stable across languages: a grouping key and a list key, never shown. */
+ val key: String,
+ /** `Calendar.MONTH`, named at draw time in whatever language the reader chose. */
+ val month: Int,
+ /** Null in the current year, where the year would be noise. */
+ val year: Int?,
+ val commits: Int,
+ val people: Int,
+ ) : FeedItem
+
+
+
+ /** Consecutive bot commits, folded. */
+ data class Bots(val count: Int, val commits: List) : FeedItem
+}
+
+object FeedLayout {
+
+ /** Below this a gap is just normal cadence and gets no label. */
+ const val QUIET_THRESHOLD_DAYS = 14
+
+ fun build(feed: CommunityFeed, installedVersionCode: Long): List {
+ val visible = feed.commits.filterNot { it.isBot }
+ if (visible.isEmpty()) return emptyList()
+
+ val bots = feed.commits.filter { it.isBot }
+ val items = ArrayList(visible.size * 2 + 8)
+
+ // Each commit's month, worked out once.
+ //
+ // This used to be a `filter` over every commit at each month boundary, with a fresh
+ // Calendar per comparison — O(commits × months), which on a six-month window was a few
+ // thousand cheap operations and on the full archive was 1517 × 55 Calendar allocations and
+ // a 1.9-second freeze on the thread laying out the feed. Two passes and a map do the same
+ // work in one sweep.
+ val months = ArrayList(visible.size)
+ val calendar = Calendar.getInstance()
+ visible.forEach { commit ->
+ calendar.timeInMillis = commit.epochSeconds * 1000
+ months += monthKey(calendar)
+ }
+ val commitsPerMonth = HashMap()
+ val peoplePerMonth = HashMap>()
+ visible.forEachIndexed { index, commit ->
+ val key = months[index]
+ commitsPerMonth[key] = (commitsPerMonth[key] ?: 0) + 1
+ val people = peoplePerMonth.getOrPut(key) { HashSet() }
+ commit.authors.forEach { if (!it.isBot) people += it.login.lowercase() }
+ }
+ val thisYear = Calendar.getInstance().get(Calendar.YEAR)
+
+ // Only meaningful once both numbers are known, and only when the reader is actually
+ // behind — telling someone who is up to date that they are up to date is noise.
+ val commitsAhead =
+ if (feed.totalCommits > 0 && installedVersionCode > 0) {
+ (feed.totalCommits - installedVersionCode).toInt().coerceAtLeast(0)
+ } else 0
+ // Past the head of master: place it at the top rather than looking for a commit it could
+ // sit above, because there is not one.
+ val aheadOfMaster = feed.totalCommits > 0 && installedVersionCode > feed.totalCommits
+ if (aheadOfMaster) {
+ items +=
+ FeedItem.InstalledMarker(
+ versionCode = installedVersionCode,
+ commitsAhead = (installedVersionCode - feed.totalCommits).toInt(),
+ aheadOfMaster = true,
+ )
+ }
+ var markerPlaced = aheadOfMaster || commitsAhead <= 0
+
+ var lastMonth: String? = null
+
+ visible.forEachIndexed { index, commit ->
+ if (!markerPlaced && commit.globalIndex <= installedVersionCode) {
+ items += FeedItem.InstalledMarker(installedVersionCode, commitsAhead)
+ markerPlaced = true
+ }
+
+ val month = months[index]
+ if (month != lastMonth) {
+ calendar.timeInMillis = commit.epochSeconds * 1000
+ val year = calendar.get(Calendar.YEAR)
+ items +=
+ FeedItem.MonthMarker(
+ key = month,
+ month = calendar.get(Calendar.MONTH),
+ year = year.takeIf { it != thisYear },
+ commits = commitsPerMonth[month] ?: 0,
+ people = peoplePerMonth[month]?.size ?: 0,
+ )
+ lastMonth = month
+ }
+
+ val older = visible.getOrNull(index + 1)
+ items +=
+ FeedItem.Commit(
+ commit = commit,
+ isFirst = index == 0,
+ isLast = older == null && bots.isEmpty(),
+ )
+
+ if (older != null) {
+ val gapDays =
+ ((commit.epochSeconds - older.epochSeconds) / 86_400L).toInt().coerceAtLeast(0)
+ items += FeedItem.Gap(gapDays, commit.sha)
+ }
+ }
+
+ if (bots.isNotEmpty()) items += FeedItem.Bots(bots.size, bots)
+ return items
+ }
+
+ /**
+ * Gap in days to rail height.
+ *
+ * Square root rather than linear. Linear is the honest chart, but this project's gaps span
+ * 0 to 76 days, so a literal scale spends a full screen of empty rail on one silence and
+ * flattens every ordinary one-to-three-day gap into the same nothing. The root keeps short
+ * gaps distinguishable, still shows a long one as visibly long, and the clamp stops any
+ * single quiet stretch from dominating the scroll.
+ */
+ fun railHeightDp(gapDays: Int): Float =
+ (MIN_GAP_DP + sqrt(gapDays.toFloat()) * SCALE).coerceAtMost(MAX_GAP_DP)
+
+ const val MIN_GAP_DP = 8f
+ const val MAX_GAP_DP = 120f
+ private const val SCALE = 13f
+
+ /**
+ * The grouping key, deliberately language-independent.
+ *
+ * This used to be the displayed name, formatted with `Locale.getDefault()` — which is the
+ * process default, the host app's, and so it stayed French while the rest of the app was in
+ * Chinese. Worse, it was computed here in the model, where the in-composition language override
+ * is not visible at all. So the model now groups by an invariant key and the screen names the
+ * month at draw time.
+ */
+ private fun monthKey(calendar: Calendar): String =
+ "%d-%02d".format(Locale.ROOT, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH))
+}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubAuth.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubAuth.kt
new file mode 100644
index 000000000..c484d382c
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubAuth.kt
@@ -0,0 +1,205 @@
+package org.matrix.vector.manager.data.github
+
+import android.content.Context
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import okhttp3.FormBody
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.matrix.vector.manager.BuildConfig
+
+/**
+ * Optional GitHub sign-in, over the **device authorisation flow**.
+ *
+ * Why this flow and not a browser redirect: it needs no redirect URI, no custom scheme and no
+ * callback into the app — which matters here because parasitically the manager has no package
+ * identity of its own to register a scheme against. The user is shown a short code, types it at
+ * github.com/login/device in any browser on any device, and this polls until it is approved.
+ *
+ * **No scopes are requested.** A token with an empty scope set can read public data and nothing
+ * else, so one leaking out of the host process's data directory cannot be used to act as the user.
+ * What it buys is the rate limit: 60 requests/hour for an anonymous client, 5000 signed in.
+ *
+ * Signing in is never required. Every surface that uses this renders without it, because a large
+ * part of this project's users cannot reach github.com at all — see [SignInState.Unavailable].
+ */
+class GitHubAuth(context: Context, private val client: OkHttpClient) {
+
+ private val prefs = context.getSharedPreferences("vector_github", Context.MODE_PRIVATE)
+
+ private val json = Json {
+ ignoreUnknownKeys = true
+ isLenient = true
+ }
+
+ private val _state = MutableStateFlow(initialState())
+ val state: StateFlow = _state.asStateFlow()
+
+ val token: String?
+ get() = prefs.getString(KEY_TOKEN, null)
+
+ /** False when no OAuth client id was compiled in, in which case the UI hides sign-in entirely. */
+ val isConfigured: Boolean
+ get() = BuildConfig.GITHUB_CLIENT_ID.isNotEmpty()
+
+ private fun initialState(): SignInState {
+ val stored = prefs.getString(KEY_TOKEN, null) ?: return SignInState.SignedOut
+ return SignInState.SignedIn(login = prefs.getString(KEY_LOGIN, null), token = stored)
+ }
+
+ /**
+ * Starts the flow and polls to completion. Emits [SignInState.AwaitingUser] with the code the
+ * user has to type, so the UI can show it and offer to copy it.
+ */
+ suspend fun signIn() {
+ if (!isConfigured) {
+ _state.value = SignInState.Unavailable("no client id")
+ return
+ }
+ withContext(Dispatchers.IO) {
+ val start =
+ runCatching { requestDeviceCode() }
+ .getOrElse {
+ // Unreachable is the expected case for a lot of users, not an error worth
+ // shouting about.
+ _state.value = SignInState.Unavailable(it.message ?: "unreachable")
+ return@withContext
+ }
+
+ _state.value =
+ SignInState.AwaitingUser(
+ userCode = start.userCode,
+ verificationUri = start.verificationUri,
+ )
+
+ val deadline = System.currentTimeMillis() + start.expiresIn * 1000L
+ var interval = start.interval.coerceAtLeast(5)
+
+ while (System.currentTimeMillis() < deadline) {
+ delay(interval * 1000L)
+ val poll = runCatching { pollForToken(start.deviceCode) }.getOrNull() ?: continue
+ when {
+ poll.accessToken != null -> {
+ val login = runCatching { fetchLogin(poll.accessToken) }.getOrNull()
+ prefs
+ .edit()
+ .putString(KEY_TOKEN, poll.accessToken)
+ .putString(KEY_LOGIN, login)
+ .apply()
+ _state.value = SignInState.SignedIn(login, poll.accessToken)
+ return@withContext
+ }
+ poll.error == "authorization_pending" -> Unit
+ poll.error == "slow_down" -> interval += 5
+ poll.error == "access_denied" -> {
+ _state.value = SignInState.SignedOut
+ return@withContext
+ }
+ else -> {
+ _state.value = SignInState.Unavailable(poll.error ?: "unknown")
+ return@withContext
+ }
+ }
+ }
+ _state.value = SignInState.SignedOut
+ }
+ }
+
+ fun signOut() {
+ prefs.edit().remove(KEY_TOKEN).remove(KEY_LOGIN).apply()
+ _state.value = SignInState.SignedOut
+ }
+
+ fun cancel() {
+ if (_state.value is SignInState.AwaitingUser) _state.value = SignInState.SignedOut
+ }
+
+ private fun requestDeviceCode(): DeviceCodeResponse {
+ val body =
+ FormBody.Builder()
+ .add("client_id", BuildConfig.GITHUB_CLIENT_ID)
+ // Deliberately empty: read-only access to public data is all this needs.
+ .add("scope", "")
+ .build()
+ val request =
+ Request.Builder()
+ .url("https://github.com/login/device/code")
+ .header("Accept", "application/json")
+ .post(body)
+ .build()
+ client.newCall(request).execute().use { response ->
+ val text = response.body.string()
+ return json.decodeFromString(text)
+ }
+ }
+
+ private fun pollForToken(deviceCode: String): TokenResponse {
+ val body =
+ FormBody.Builder()
+ .add("client_id", BuildConfig.GITHUB_CLIENT_ID)
+ .add("device_code", deviceCode)
+ .add("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
+ .build()
+ val request =
+ Request.Builder()
+ .url("https://github.com/login/oauth/access_token")
+ .header("Accept", "application/json")
+ .post(body)
+ .build()
+ client.newCall(request).execute().use { response ->
+ val text = response.body.string()
+ return json.decodeFromString(text)
+ }
+ }
+
+ private fun fetchLogin(token: String): String? {
+ val request =
+ Request.Builder()
+ .url("https://api.github.com/user")
+ .header("Authorization", "Bearer $token")
+ .header("Accept", "application/vnd.github+json")
+ .build()
+ client.newCall(request).execute().use { response ->
+ if (!response.isSuccessful) return null
+ return json.decodeFromString(response.body.string()).login
+ }
+ }
+
+ private companion object {
+ const val KEY_TOKEN = "token"
+ const val KEY_LOGIN = "login"
+ }
+}
+
+sealed interface SignInState {
+ data object SignedOut : SignInState
+
+ data class AwaitingUser(val userCode: String, val verificationUri: String) : SignInState
+
+ data class SignedIn(val login: String?, val token: String) : SignInState
+
+ /** GitHub could not be reached, or no client id was compiled in. Not an error state. */
+ data class Unavailable(val reason: String) : SignInState
+}
+
+@Serializable
+private data class DeviceCodeResponse(
+ @SerialName("device_code") val deviceCode: String,
+ @SerialName("user_code") val userCode: String,
+ @SerialName("verification_uri") val verificationUri: String,
+ @SerialName("expires_in") val expiresIn: Int = 900,
+ val interval: Int = 5,
+)
+
+@Serializable
+private data class TokenResponse(
+ @SerialName("access_token") val accessToken: String? = null,
+ val error: String? = null,
+)
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubModels.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubModels.kt
new file mode 100644
index 000000000..d9c8d2b67
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubModels.kt
@@ -0,0 +1,349 @@
+package org.matrix.vector.manager.data.github
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * The slice of GitHub's commit payload the Home feed needs.
+ *
+ * One request to `/commits?since=…` carries everything: the author's login and avatar for the
+ * contributor row, the date for the timeline, the subject line, and the SHA. There is deliberately
+ * no second call to `/contributors` — the people are derived from the commits, which both halves
+ * the rate-limit spend and makes the row mean "who worked on this *recently*" rather than an
+ * all-time leaderboard nobody reads twice.
+ */
+@Serializable
+data class GhCommit(
+ val sha: String,
+ val commit: GhCommitDetail,
+ val author: GhUser? = null,
+ @SerialName("html_url") val htmlUrl: String? = null,
+)
+
+@Serializable
+data class GhCommitDetail(
+ val message: String,
+ val author: GhCommitAuthor,
+ /**
+ * When the commit landed, as opposed to when it was written.
+ *
+ * The two differ on 39 of the newest 100 commits here, by up to four days — anything rebased,
+ * cherry-picked or merged from a branch that sat for a while. It matters because `since` and
+ * `until` filter on *this* date, so it is the only correct cursor for walking history
+ * backwards. Walking on the author date would step past commits written before the boundary
+ * but landed after it, and lose them silently.
+ */
+ val committer: GhCommitAuthor? = null,
+)
+
+@Serializable
+data class GhCommitAuthor(
+ val name: String,
+ val date: String,
+ /** Present on every commit, and the only handle on an author GitHub failed to link. */
+ val email: String = "",
+)
+
+@Serializable
+data class GhUser(
+ val login: String,
+ @SerialName("avatar_url") val avatarUrl: String? = null,
+ @SerialName("html_url") val htmlUrl: String? = null,
+ val type: String? = null,
+)
+
+@Serializable
+data class GhRepo(
+ @SerialName("stargazers_count") val stars: Int = 0,
+ @SerialName("forks_count") val forks: Int = 0,
+ @SerialName("open_issues_count") val openIssues: Int = 0,
+ val license: GhLicense? = null,
+)
+
+@Serializable data class GhLicense(@SerialName("spdx_id") val spdxId: String? = null)
+
+/** How a commit subject is classified. Vector writes plain imperative subjects, not
+ * conventional-commit prefixes, so the leading verb is what gets read. Validated over the last 300
+ * commits: `Fix` 56, `Bump`/`Update`/`Upgrade` 88, `Add`/`New`/`Implement` 22, `Remove`/`Delete`
+ * 14. */
+enum class CommitKind {
+ Fix,
+ Add,
+ Remove,
+ Chore,
+ Change;
+
+ companion object {
+ fun of(subject: String): CommitKind =
+ when (subject.substringBefore(' ').trimStart('[').lowercase()) {
+ "fix",
+ "fixes",
+ "fixed" -> Fix
+ "add",
+ "adds",
+ "new",
+ "implement",
+ "introduce",
+ "allow",
+ "support" -> Add
+ "remove",
+ "removes",
+ "delete",
+ "drop" -> Remove
+ "bump",
+ "update",
+ "upgrade",
+ "migrate",
+ "translation]",
+ "translation" -> Chore
+ else -> Change
+ }
+ }
+}
+
+/**
+ * One person credited on a commit — the author, or anyone named in a `Co-authored-by:` trailer.
+ *
+ * Co-authors are real contributors and are counted as such. GitHub's commits API does not return
+ * them as users, so they are parsed out of the message; when the trailer carries a GitHub noreply
+ * address the login and avatar can be recovered from it exactly, and otherwise the person is shown
+ * under the name they signed with and a monogram.
+ */
+data class CommitPerson(
+ val login: String,
+ val avatarUrl: String?,
+ val profileUrl: String?,
+ val isBot: Boolean = false,
+)
+
+/** A commit as the timeline renders it. */
+data class TimelineCommit(
+ val sha: String,
+ val shortSha: String,
+ val subject: String,
+ val kind: CommitKind,
+ /** Pull-request number parsed from a trailing `(#123)`, the link to the discussion. */
+ val pullRequest: Int?,
+ /** The author first, then any co-authors, deduplicated. */
+ val authors: List,
+ val epochSeconds: Long,
+ val htmlUrl: String?,
+ /**
+ * Distance from the repository's first commit, i.e. this commit's own version number —
+ * `versionCode` is generated by `git rev-list --count`, so the two are the same scale and a
+ * build can be located on the timeline exactly.
+ */
+ val globalIndex: Long,
+ /** True when anyone credited is not the repository owner — highlighted on the rail. */
+ val isCommunity: Boolean,
+ val isBot: Boolean,
+) {
+ val authorLogin: String
+ get() = authors.firstOrNull()?.login.orEmpty()
+
+ val coAuthors: List
+ get() = authors.drop(1)
+}
+
+/** A person credited inside the window, with how much and how recently. */
+data class Contributor(
+ val login: String,
+ val avatarUrl: String?,
+ val profileUrl: String?,
+ val commits: Int,
+ /** Their most recent commit in the window; breaks ties so the row stays a live scoreboard. */
+ val lastEpochSeconds: Long,
+)
+
+/** Everything the Home community section renders, or the reason it cannot. */
+data class CommunityFeed(
+ val commits: List = emptyList(),
+ val contributors: List = emptyList(),
+ val windowStartEpochSeconds: Long = 0,
+ val repo: GhRepo? = null,
+ /** Commits on the default branch, ever. Equals the newest build's versionCode. */
+ val totalCommits: Long = 0,
+ /** True when this came off disk rather than the network. */
+ /** True when this came off disk rather than the network, for any reason. */
+ val fromCache: Boolean = false,
+ /**
+ * True only when the network was actually tried and could not be reached.
+ *
+ * Not the same as [fromCache]. Home deliberately reads the cache on most launches, so telling
+ * the user "could not reach GitHub" whenever the feed came from disk announced a failure that
+ * had not happened — the app simply had not asked.
+ */
+ val offline: Boolean = false,
+ /**
+ * False until the first load resolves. Without it the initial empty value is indistinguishable
+ * from a genuinely empty result, and the page claims "no commits" before it has looked.
+ */
+ val loaded: Boolean = false,
+ /**
+ * True while the archive has not yet been walked back as far as this window reaches.
+ *
+ * The distinction the feed's foot depends on: more to fetch means an invitation to keep
+ * scrolling, nothing more to fetch means the rail has genuinely reached the first commit and
+ * says so.
+ */
+ val hasMoreHistory: Boolean = false,
+) {
+ val commitCount: Int
+ get() = commits.size
+
+ val isEmpty: Boolean
+ get() = commits.isEmpty()
+
+ /**
+ * The same feed narrowed to the commits [logins] took part in.
+ *
+ * Co-authorship counts, which is the whole point of filtering here rather than linking out to
+ * GitHub's author filter: GitHub's own view is by *author*, so a contribution that landed under
+ * a maintainer's name with the contributor credited in a trailer does not appear under the
+ * contributor at all. Here it does, because the rail already knows every name on a commit.
+ *
+ * Only the commits are narrowed. The contributor row is the control — filtering it by its own
+ * selection would remove the people you would need to tap to change it.
+ */
+ fun filteredBy(logins: Set): CommunityFeed =
+ if (logins.isEmpty()) this
+ else
+ copy(
+ commits =
+ commits.filter { commit ->
+ commit.authors.any { it.login.lowercase() in logins }
+ }
+ )
+}
+
+// --- CI builds ------------------------------------------------------------------------------
+
+@Serializable
+data class GhRelease(
+ val id: Long,
+ @SerialName("tag_name") val tagName: String = "",
+ val name: String? = null,
+ val prerelease: Boolean = false,
+ @SerialName("target_commitish") val targetCommitish: String = "",
+ @SerialName("published_at") val publishedAt: String? = null,
+ @SerialName("html_url") val htmlUrl: String? = null,
+ val body: String? = null,
+ val assets: List = emptyList(),
+)
+
+@Serializable
+data class GhReleaseAsset(
+ val id: Long,
+ val name: String,
+ val size: Long = 0,
+ @SerialName("browser_download_url") val downloadUrl: String? = null,
+)
+
+@Serializable
+data class GhWorkflowRuns(
+ @SerialName("workflow_runs") val runs: List = emptyList()
+)
+
+@Serializable
+data class GhWorkflowRun(
+ val id: Long,
+ val name: String? = null,
+ @SerialName("head_branch") val headBranch: String? = null,
+ @SerialName("head_sha") val headSha: String = "",
+ @SerialName("display_title") val displayTitle: String? = null,
+ @SerialName("created_at") val createdAt: String = "",
+ @SerialName("html_url") val htmlUrl: String? = null,
+ val conclusion: String? = null,
+)
+
+@Serializable
+data class GhArtifacts(val artifacts: List = emptyList())
+
+@Serializable
+data class GhArtifact(
+ val id: Long,
+ val name: String,
+ @SerialName("size_in_bytes") val sizeInBytes: Long = 0,
+ val expired: Boolean = false,
+ @SerialName("archive_download_url") val downloadUrl: String? = null,
+)
+
+/** A successful CI run, as the canary screen renders it. */
+data class CanaryBuild(
+ val id: Long,
+ val title: String,
+ val branch: String,
+ val shortSha: String,
+ val epochSeconds: Long,
+ val htmlUrl: String?,
+ val artifacts: List,
+)
+
+/**
+ * A published build of the framework, canary or stable, with the zip to flash.
+ *
+ * One type for both channels because the install path is identical — the difference is only which
+ * of them a given reader is allowed to be offered.
+ */
+data class FrameworkRelease(
+ val tag: String,
+ val title: String,
+ val versionCode: Long,
+ val isCanary: Boolean,
+ val notesMarkdown: String?,
+ val htmlUrl: String?,
+ val epochSeconds: Long,
+ /**
+ * The commit the release was cut from, when GitHub knows one.
+ *
+ * `target_commitish` is a full SHA for the canaries, because CI creates them against an exact
+ * commit — but it is the literal string "master" for a hand-made release, which names a branch
+ * and not a build. Only the first kind can be compared against, and the difference between
+ * "these differ" and "I cannot tell" is one the UI has to keep.
+ */
+ val commit: String?,
+ /**
+ * Every zip the release published, in the order GitHub listed them.
+ *
+ * A list rather than the single "first zip" this used to keep: each release ships a Release
+ * and a Debug build, 8 MB against 22 MB, and picking whichever GitHub happened to list first
+ * meant the reader could neither choose nor tell which one they were about to flash. The app
+ * asks people for a debug build when they report a problem, so it has to be able to install
+ * one.
+ */
+ val zips: List,
+) {
+ /** The one to offer by default when nothing has been chosen. */
+ val defaultZip: CanaryArtifact?
+ get() = zips.firstOrNull { it.variant == ZipVariant.Release } ?: zips.firstOrNull()
+}
+
+/**
+ * Which build a zip is, read from its file name.
+ *
+ * [Other] is not a failure: a release may one day publish something these two names do not cover,
+ * and a picker that cannot represent it would either hide the file or mislabel it. Both are worse
+ * than showing the name it actually has.
+ */
+enum class ZipVariant(val key: String) {
+ Release("release"),
+ Debug("debug"),
+ Other("other"),
+}
+
+data class CanaryArtifact(
+ val id: Long,
+ val name: String,
+ val sizeInBytes: Long,
+ val expired: Boolean,
+ val downloadUrl: String?,
+) {
+ val variant: ZipVariant
+ get() =
+ when {
+ name.contains("release", ignoreCase = true) -> ZipVariant.Release
+ name.contains("debug", ignoreCase = true) -> ZipVariant.Debug
+ else -> ZipVariant.Other
+ }
+}
+
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubRepository.kt
new file mode 100644
index 000000000..8c02f3711
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubRepository.kt
@@ -0,0 +1,903 @@
+package org.matrix.vector.manager.data.github
+
+import java.io.File
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import okhttp3.CacheControl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+
+/**
+ * Loads the last quarter of activity on the project's GitHub repository.
+ *
+ * Offline-first: [load] returns whatever is on disk immediately if the network fails, and the
+ * caller renders it with a "showing cached" affordance rather than an error. A framework manager
+ * must never be blocked by GitHub being unreachable.
+ */
+class GitHubRepository(
+ private val client: OkHttpClient,
+ cacheDir: File,
+ /**
+ * Supplies the optional sign-in token. Anonymous access is a fully supported mode — this only
+ * ever raises the rate limit from 60 to 5000 requests an hour.
+ */
+ private val tokenProvider: () -> String? = { null },
+ /** How far back to reach, in months. User-configurable; see SettingsRepository. */
+ private val windowMonthsProvider: () -> Int = { DEFAULT_WINDOW_MONTHS },
+) {
+
+ private val snapshotFile = File(cacheDir, "github_feed.json")
+ private val peopleFile = File(cacheDir, "github_people.json")
+
+ /**
+ * The stars, forks and licence, in a file of their own.
+ *
+ * They used to live inside the feed snapshot, which is why they kept disappearing: the feed is
+ * rewritten on every successful commit fetch, and the repo comes from a *separate* request that
+ * fails independently — so one rate-limited hour replaced a perfectly good answer with nothing,
+ * and every later launch wrote the nothing back. Merging on write fixed that particular loss,
+ * but it left the answer hostage to a file that something else owns and rewrites constantly.
+ *
+ * Kept apart, they can only be replaced by an actual answer to the question they came from.
+ * Nothing else writes here, so nothing else can lose them. They are also the slowest-changing
+ * thing on the screen — a star count from yesterday is not wrong in any way a reader cares
+ * about — so serving a stale one indefinitely is the correct behaviour, not a fallback.
+ */
+ private val repoFile = File(cacheDir, "github_repo.json")
+
+ /**
+ * The whole history, once it has been walked.
+ *
+ * Separate from the feed snapshot on purpose: the snapshot is one window's worth and is
+ * replaced wholesale, while this only ever grows. See [CommitArchive] for why it is
+ * append-only and keyed by SHA.
+ */
+ private val archive by lazy {
+ CommitArchive(
+ File(cacheDir, "github_history.ndjson"),
+ File(cacheDir, "github_history_state.json"),
+ json,
+ )
+ }
+
+ private fun readRepo(): GhRepo? =
+ runCatching { json.decodeFromString(repoFile.readText()) }.getOrNull()
+
+ private fun writeRepo(repo: GhRepo?) {
+ if (repo == null) return
+ runCatching { repoFile.writeText(json.encodeToString(repo)) }
+ }
+
+ /**
+ * Names we have already tried to resolve to a GitHub account, and what came back.
+ *
+ * A null value is a remembered *failure*: it is as worth keeping as a success, because the
+ * alternative is asking about the same unresolvable name on every load. Persisted so that
+ * survives the process, which parasitically is killed often.
+ */
+ private val resolvedPeople: MutableMap by lazy {
+ runCatching { json.decodeFromString>(peopleFile.readText()) }
+ .getOrDefault(emptyMap())
+ .toMutableMap()
+ }
+
+ private val json = Json {
+ ignoreUnknownKeys = true
+ isLenient = true
+ }
+
+ /**
+ * How hard to try for fresh data.
+ *
+ * Opening Home is not a reason to talk to GitHub. The window it shows moves a few times a
+ * week at most, so revalidating on every launch spends the user's battery and their share of
+ * an anonymous rate limit to redraw the same rows.
+ */
+ enum class Freshness {
+ /** Disk only. Never touches the network. */
+ Cached,
+ /** Normal conditional request; a 304 costs nothing against the rate limit. */
+ Revalidate,
+ /** Ignore caches entirely. Only for an explicit pull-to-refresh. */
+ Force,
+ }
+
+ suspend fun load(freshness: Freshness = Freshness.Revalidate): CommunityFeed =
+ withContext(Dispatchers.IO) {
+ // Zero means "as far back as there is", which is a different question from "how many
+ // months". Either way this call fetches exactly one page — the newest hundred — and the
+ // rest of history comes from [archive], which [backfill] fills in a few pages at a time
+ // across sessions. The line under the feed says how far back the answer actually
+ // reaches, rather than claiming the project began there.
+ val months = windowMonthsProvider().coerceIn(0, 60)
+ val windowStart =
+ if (months == 0) 0L
+ else System.currentTimeMillis() / 1000 - months * DAYS_PER_MONTH * 24L * 60 * 60
+ val previous =
+ runCatching { json.decodeFromString(snapshotFile.readText()) }.getOrNull()
+ val fresh = runCatching { fetch(windowStart, freshness) }.getOrNull()
+ if (fresh != null) {
+ // Merged with what was already on disk rather than replacing it. The stars, forks
+ // and licence come from a *second* request, and the two do not fail together: on a
+ // rate-limited hour the commits arrived and the repo did not, and writing that
+ // straight through erased a perfectly good cached answer — the footer disappeared
+ // and stayed disappeared, because every later launch overwrote it again. A field
+ // this fetch could not answer keeps the last answer that was.
+ writeRepo(fresh.repo)
+ // The head window is the mutable part of history — it can be amended or rebased —
+ // so it is appended every time and the duplicates resolved on read.
+ archive.append(fresh.rawCommits)
+ val repo = fresh.repo ?: readRepo() ?: previous?.repo
+ val total = if (fresh.totalCommits > 0) fresh.totalCommits else previous?.totalCommits ?: 0L
+ runCatching {
+ snapshotFile.writeText(
+ json.encodeToString(Snapshot(total, fresh.rawCommits, repo))
+ )
+ }
+ return@withContext build(
+ timeline(fresh.rawCommits, windowStart),
+ repo,
+ windowStart,
+ fromCache = false,
+ offline = false,
+ totalCommits = total,
+ freshness = freshness,
+ )
+ }
+ val cached =
+ runCatching { json.decodeFromString(snapshotFile.readText()) }
+ // A file written before the total was stored still parses as a bare list, and
+ // is worth reading — it only costs the "you are here" line until the next fetch.
+ .recoverCatching {
+ Snapshot(
+ commits = json.decodeFromString>(snapshotFile.readText())
+ )
+ }
+ .getOrNull()
+ ?: return@withContext CommunityFeed(
+ fromCache = true,
+ // Nothing on disk and nothing from the network: whether that counts as
+ // offline still depends on whether a request was even attempted.
+ offline = freshness != Freshness.Cached,
+ loaded = true,
+ )
+ build(
+ timeline(cached.commits, windowStart),
+ // Its own file first: the feed snapshot's copy is a historical accident and may
+ // predate the split, or have been nulled by the bug that prompted it.
+ readRepo() ?: cached.repo,
+ windowStart,
+ fromCache = true,
+ offline = freshness != Freshness.Cached,
+ // Kept with the commits rather than recomputed: it comes from the `Link` header of
+ // a request this path did not make. Without it every cached read lost the commit
+ // numbering, and "you are here" — which is the whole point of numbering them —
+ // silently vanished on the four launches out of five that read from disk.
+ totalCommits = cached.totalCommits,
+ )
+ }
+
+ /**
+ * The commits the feed should render, from the archive where it reaches and [fallback] where it
+ * does not.
+ *
+ * Both sources are used rather than one: the archive is authoritative — it is the only thing
+ * that reaches past the newest hundred — but it lives in the cache directory and can be cleared
+ * out from under us at any moment, in which case the page must still show what it just fetched
+ * rather than nothing. Merging by SHA makes the overlap between them free.
+ *
+ * The window is applied here, at the end, so it is a view of the archive rather than a limit on
+ * what is kept. Narrowing the window is then instant and costs no request, and widening it
+ * later finds the history already on disk.
+ */
+ private fun timeline(fallback: List, windowStart: Long): List {
+ val merged = LinkedHashMap()
+ fallback.forEach { merged[it.sha] = it }
+ archive.read().forEach { merged[it.sha] = it }
+ val all = merged.values.sortedByDescending { it.commit.author.date }
+ // Learned from the whole archive, not from the window, so a co-author trailer in a recent
+ // commit can be resolved by an attribution GitHub made three years ago.
+ identities = identityIndex(all)
+ return if (windowStart <= 0) all
+ else all.filter { parseIso8601(it.commit.author.date) >= windowStart }
+ }
+
+ /**
+ * Addresses and names that GitHub has already told us belong to an account.
+ *
+ * The problem this solves is co-author trailers. `Co-authored-by: Someone `
+ * carries no account, and there is no API that turns an address into one — the users search
+ * endpoint deliberately refuses to index email addresses, and answers `total_count: 0` for a
+ * `@users.noreply.github.com` one no matter how it is phrased. So the address either resolves
+ * from something already in hand or it does not resolve at all.
+ *
+ * Something already in hand is exactly what the archive is. Every commit carries both the git
+ * identity that wrote it — name and email — and, when GitHub could match that identity to an
+ * account, the account itself. Every such commit is therefore a verified email-to-login pair,
+ * published by the only party in a position to know. Someone who co-authored one commit has
+ * very often authored another; indexing the pairs makes the second commit answer the first.
+ *
+ * The cost is one pass over a list already in memory, and no request at all. Names are indexed
+ * too, one tier weaker: a display name is not unique and is claimed first-wins, which is the
+ * right trade for a credit line but would not be for anything that mattered more.
+ */
+ private fun identityIndex(commits: List): Map {
+ val index = HashMap()
+ commits.forEach { c ->
+ val user = c.author ?: return@forEach
+ val person =
+ CommitPerson(
+ login = user.login,
+ avatarUrl = user.avatarUrl,
+ profileUrl = user.htmlUrl,
+ isBot = user.type == "Bot" || user.login.endsWith("[bot]"),
+ )
+ c.commit.author.email.lowercase().takeIf { it.isNotEmpty() }?.let {
+ index.putIfAbsent(it, person)
+ }
+ c.commit.author.name.lowercase().takeIf { it.isNotEmpty() }?.let {
+ index.putIfAbsent(it, person)
+ }
+ }
+ return index
+ }
+
+ /** Rebuilt on every load from the archive; empty until the first one. */
+ @Volatile private var identities: Map = emptyMap()
+
+ /**
+ * Every commit on the default branch, ever, as GitHub last reported it.
+ *
+ * From the `Link: rel="last"` header of a one-per-page request — a number the walk can be
+ * checked against. Holding that many unique commits *is* holding the history, which is a
+ * better answer than "a page came back empty": it is known before the request that would have
+ * proved it, so a finished archive stops asking rather than asking once more to be told no.
+ */
+ @Volatile private var knownTotalCommits: Long = 0
+
+ /**
+ * Walks the history backwards, a page at a time, and stops.
+ *
+ * ## The algorithm
+ *
+ * The cursor is the commit date of the oldest commit held, and each request asks for commits at
+ * or before it — `until=`, not `page=`. Page numbers are the obvious choice and the wrong one:
+ * they are relative to the tip, so a single new commit landing mid-walk shifts every boundary
+ * and silently skips or repeats a page. A date is absolute. The cost of that choice is that
+ * the boundary commit comes back again on the next request, which is harmless because the
+ * archive is keyed by SHA.
+ *
+ * The *commit* date, not the author date, because that is what `until` filters on and the two
+ * are not the same — 39 of the newest hundred commits here differ, by up to four days. A cursor
+ * on the author date would ask for commits before a moment that had already passed for some of
+ * them, and they would never be seen again.
+ *
+ * A date cursor has one failure mode, and this repository has it. Squashes and imports stamp
+ * many commits with the same second — 100+ of these share `2023-02-26T08:48:49Z` — and a plateau
+ * wider than one page is a wall the cursor cannot climb: every request returns the same hundred,
+ * and the walk stops a fifth of the way through history believing it is done. So when a page
+ * fails to move the cursor, the walk pages by number *within that timestamp* until it does.
+ * Numbered paging is safe in exactly this position and nowhere else: the window is anchored by
+ * an `until` in the past, so commits landing now fall outside it and cannot shift it.
+ *
+ * Completion is an *empty* page, and nothing weaker. "Nothing new in this page" is what the
+ * plateau produces on every request, and reading it as the end was precisely the bug; "fewer
+ * than a hundred" is what a shared boundary second produces legitimately.
+ *
+ * ## Why it stops early
+ *
+ * An anonymous client gets sixty requests an hour and a few thousand commits is thirty of them.
+ * So this fetches a handful of pages and returns, leaving the cursor on disk for the next call
+ * — from the next launch, or from the reader scrolling towards the end of the list. A history
+ * that assembles over a few sessions is fine; one that spends someone's entire rate limit in a
+ * single launch, and then leaves the store empty for an hour, is not.
+ *
+ * Returns the number of commits genuinely new to the archive.
+ */
+ suspend fun backfill(maxPages: Int = 3): Int =
+ withContext(Dispatchers.IO) {
+ var state = archive.state()
+ if (state.complete) return@withContext 0
+
+ val known = archive.read()
+ // The cheapest completion test there is, and the only one that does not cost a
+ // request: the project has a known number of commits and this holds that many. It also
+ // covers the case the page-walk cannot — an archive assembled across several sessions
+ // whose last page happened to end exactly on the first commit, which otherwise stays
+ // "incomplete" forever and re-asks on every visit to the foot of the feed.
+ if (knownTotalCommits > 0 && known.size >= knownTotalCommits) {
+ archive.writeState(state.copy(complete = true))
+ return@withContext 0
+ }
+ val seen = known.mapTo(mutableSetOf()) { it.sha }
+ var cursor =
+ state.oldestSeenEpochSeconds.takeIf { it > 0 }
+ ?: known.minOfOrNull { cursorDateOf(it) }
+ ?: return@withContext 0
+ var page = state.pageWithinCursor.coerceAtLeast(1)
+
+ var added = 0
+ repeat(maxPages) {
+ val batch =
+ runCatching {
+ get(
+ "$API/$REPO/commits?until=${iso8601(cursor)}&per_page=100&page=$page",
+ Freshness.Revalidate,
+ )
+ ?.let { json.decodeFromString>(it) }
+ }
+ .getOrNull()
+ // A refused or failed request is not the end of history. Leaving `complete` false
+ // means the next call tries again rather than declaring the archive finished
+ // because GitHub was rate limiting at the time.
+ if (batch == null) return@repeat
+
+ if (batch.isEmpty()) {
+ state = state.copy(complete = true)
+ archive.writeState(state)
+ archive.compactIfWasteful()
+ return@withContext added
+ }
+
+ val fresh = batch.filterNot { it.sha in seen }
+ if (fresh.isNotEmpty()) {
+ archive.append(fresh)
+ fresh.forEach { seen += it.sha }
+ added += fresh.size
+ }
+
+ // Whether the cursor can move is decided by the whole page, not by the new part of
+ // it: inside a plateau every commit is already known and the oldest date is
+ // unchanged, which is exactly the case the page number exists to get past.
+ val oldest = batch.minOf { cursorDateOf(it) }
+ if (oldest < cursor) {
+ cursor = oldest
+ page = 1
+ } else {
+ page++
+ }
+ state =
+ state.copy(
+ oldestSeenEpochSeconds = cursor,
+ pageWithinCursor = page,
+ pagesFetched = state.pagesFetched + 1,
+ )
+ archive.writeState(state)
+ }
+ archive.compactIfWasteful()
+ added
+ }
+
+ /** The date `until` understands: when the commit landed, falling back to when it was written. */
+ private fun cursorDateOf(commit: GhCommit): Long =
+ parseIso8601(commit.commit.committer?.date ?: commit.commit.author.date)
+
+ /**
+ * What the feed file holds.
+ *
+ * The total is stored beside the commits because it cannot be derived from them: it comes from
+ * the `Link: rel="last"` header of the commit request, and a cached read makes no request.
+ */
+ @Serializable
+ private data class Snapshot(
+ val totalCommits: Long = 0L,
+ val commits: List = emptyList(),
+ /**
+ * The stars, forks and licence line.
+ *
+ * Stored for the same reason as the total: it comes from a second request, and a cached
+ * read makes none — so without it the "take part" numbers vanished on every launch that
+ * deliberately did not fetch, which is most of them.
+ */
+ val repo: GhRepo? = null,
+ )
+
+ private class Fetched(
+ val rawCommits: List,
+ val repo: GhRepo?,
+ val totalCommits: Long,
+ )
+
+ private fun fetch(windowStartEpochSeconds: Long, freshness: Freshness): Fetched {
+ val since = iso8601(windowStartEpochSeconds)
+ val commits =
+ get("$API/$REPO/commits?since=$since&per_page=100", freshness)?.let {
+ json.decodeFromString>(it)
+ } ?: throw IllegalStateException("commits unavailable")
+
+ // The repo stats are a nice-to-have; a failure here must not lose the commits.
+ val repo =
+ runCatching { get("$API/$REPO", freshness)?.let { json.decodeFromString(it) } }
+ .getOrNull()
+
+ val total = runCatching { fetchTotalCommits() }.getOrDefault(0L)
+ return Fetched(commits, repo, total)
+ }
+
+ /**
+ * How many commits the default branch has, ever.
+ *
+ * There is no field for this, but asking for one commit per page makes GitHub report the last
+ * page number in its `Link` header, and that number is the count. One cheap request, and it
+ * is what makes "you are N commits behind" exact rather than a guess.
+ */
+ private fun fetchTotalCommits(): Long {
+ val request =
+ Request.Builder()
+ .url("$API/$REPO/commits?per_page=1")
+ .header("Accept", "application/vnd.github+json")
+ .apply { tokenProvider()?.let { header("Authorization", "Bearer $it") } }
+ .build()
+ client.newCall(request).execute().use { response ->
+ val link = response.header("Link") ?: return 0L
+ return LAST_PAGE.find(link)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0L
+ }
+ }
+
+ private fun get(url: String, freshness: Freshness): String? {
+ val request =
+ Request.Builder()
+ .url(url)
+ .header("Accept", "application/vnd.github+json")
+ .header("X-GitHub-Api-Version", "2022-11-28")
+ .apply { tokenProvider()?.let { header("Authorization", "Bearer $it") } }
+ .apply {
+ // OkHttp replays the stored ETag as If-None-Match on its own. A 304 costs
+ // nothing against GitHub's 60/hour budget, so revalidation stays cheap.
+ cacheControl(
+ when (freshness) {
+ Freshness.Force -> CacheControl.FORCE_NETWORK
+ Freshness.Cached -> CacheControl.FORCE_CACHE
+ Freshness.Revalidate ->
+ CacheControl.Builder()
+ .maxAge(REVALIDATE_MINUTES.toInt(), TimeUnit.MINUTES)
+ .build()
+ }
+ )
+ }
+ .build()
+
+ client.newCall(request).execute().use { response ->
+ if (!response.isSuccessful) return null
+ return response.body.string()
+ }
+ }
+
+ private fun build(
+ raw: List,
+ repo: GhRepo?,
+ windowStart: Long,
+ fromCache: Boolean,
+ offline: Boolean,
+ totalCommits: Long,
+ freshness: Freshness = Freshness.Cached,
+ ): CommunityFeed {
+ if (totalCommits > 0) knownTotalCommits = totalCommits
+ val commits =
+ raw.map { c ->
+ val subject = c.commit.message.lineSequence().first().trim()
+ val primary =
+ c.author?.let {
+ CommitPerson(
+ login = it.login,
+ avatarUrl = it.avatarUrl,
+ profileUrl = it.htmlUrl,
+ isBot = it.type == "Bot" || it.login.endsWith("[bot]"),
+ )
+ }
+ // GitHub links a commit to an account by email, and does not always
+ // manage it — a commit written under an address the account never
+ // verified arrives with no author at all. The trailer path already knows
+ // how to make something of a name and an address, so it is reused here
+ // rather than dropping the person to a bare string.
+ ?: person(c.commit.author.name, c.commit.author.email, freshness)
+
+ // Everyone credited, author first, deduplicated case-insensitively — the
+ // maintainer often appears in a trailer on their own commits.
+ val authors =
+ (listOf(primary) + coAuthors(c.commit.message, freshness)).distinctBy {
+ it.login.lowercase()
+ }
+
+ TimelineCommit(
+ sha = c.sha,
+ shortSha = c.sha.take(7),
+ subject = subject.removeSuffix(" (#${prNumber(subject) ?: ""})").trim(),
+ kind = CommitKind.of(subject),
+ pullRequest = prNumber(subject),
+ authors = authors,
+ epochSeconds = parseIso8601(c.commit.author.date),
+ htmlUrl = c.htmlUrl,
+ globalIndex = 0, // assigned after sorting, below
+
+ // Collaboration counts: a commit the maintainer landed with an outside
+ // co-author is a community contribution and is highlighted as one.
+ isCommunity =
+ authors.any {
+ !it.isBot && !it.login.equals(OWNER, ignoreCase = true)
+ },
+ isBot = primary.isBot,
+ )
+ }
+ .sortedByDescending { it.epochSeconds }
+ // Newest-first, so the head of the list is the newest commit and its distance
+ // from the repository root is the total count.
+ .mapIndexed { index, commit -> commit.copy(globalIndex = totalCommits - index) }
+
+ // Credit follows people, not commits: a co-author is a contributor. Bots are excluded —
+ // automation is not a contributor.
+ val contributors =
+ commits
+ .flatMap { commit -> commit.authors.map { person -> person to commit.epochSeconds } }
+ .filterNot { (person, _) -> person.isBot }
+ .groupBy { (person, _) -> person.login.lowercase() }
+ .map { (_, entries) ->
+ val people = entries.map { it.first }
+ Contributor(
+ login = people.first().login,
+ avatarUrl = people.firstNotNullOfOrNull { it.avatarUrl },
+ profileUrl = people.firstNotNullOfOrNull { it.profileUrl },
+ commits = entries.size,
+ lastEpochSeconds = entries.maxOf { it.second },
+ )
+ }
+ // Ties break on recency, so among equals the person who contributed most
+ // recently is shown first — the row is meant to move.
+ .sortedWith(
+ compareByDescending { it.commits }
+ .thenByDescending { it.lastEpochSeconds }
+ .thenBy { it.login }
+ )
+
+ return CommunityFeed(
+ commits = commits,
+ contributors = contributors,
+ // The oldest commit actually in hand, not the window that was asked for. They differ
+ // whenever the window reaches further back than the history does — always, for an
+ // unbounded window — and "since 1 January 1970" would be a strange thing to tell
+ // someone. Saying how far the data really goes is both honest and more useful.
+ windowStartEpochSeconds =
+ commits.minOfOrNull { it.epochSeconds }?.coerceAtLeast(windowStart) ?: windowStart,
+ repo = repo,
+ totalCommits = totalCommits,
+ fromCache = fromCache,
+ offline = offline,
+ loaded = true,
+ // Two ways the window can be under-served, and both mean "keep walking": an unbounded
+ // window is never covered until history runs out, and a bounded one is uncovered while
+ // the oldest commit in hand is still newer than its start — which happens whenever the
+ // window holds more than the hundred commits a single request returns.
+ hasMoreHistory =
+ !archive.state().complete &&
+ !(totalCommits > 0 && archive.read().size >= totalCommits) &&
+ (windowStart <= 0 ||
+ (commits.minOfOrNull { it.epochSeconds } ?: 0L) > windowStart),
+ )
+ }
+
+ /**
+ * Parses `Co-authored-by: Name ` trailers.
+ *
+ * When the address is a GitHub noreply one the login is exactly the local part, and the
+ * numeric prefix — `44231502+byemaxx@users.noreply.github.com` — is the user id, which yields
+ * the real avatar. Otherwise [identityIndex] is asked whether this project has seen the address
+ * attributed before, which resolves most of the rest for free. Only when all of that fails is
+ * the person shown under the name they signed with, with a monogram rather than being dropped.
+ */
+ private fun coAuthors(message: String, freshness: Freshness): List =
+ CO_AUTHOR.findAll(message)
+ .map { match -> person(match.groupValues[1].trim(), match.groupValues[2].trim(), freshness) }
+ .toList()
+
+ /**
+ * The best account we can make of a name and an email address.
+ *
+ * Four tiers, cheapest and most certain first: an address GitHub has already attributed
+ * somewhere in the archive is simply that account; a `@users.noreply.github.com` address *is*
+ * the account and needs nothing but parsing; a name seen attributed before costs nothing
+ * either; a handle-shaped name is worth one lookup. Failing all four, the person is shown under
+ * the name they signed with, uncredited but not dropped.
+ */
+ private fun person(name: String, email: String, freshness: Freshness): CommitPerson {
+ // Before anything else, and before any request: an address this project has seen attributed
+ // is settled, and no amount of guessing improves on GitHub's own answer.
+ identities[email.lowercase()]?.let {
+ return it
+ }
+ val noreply = GITHUB_NOREPLY.find(email)
+ if (noreply != null) {
+ val id = noreply.groupValues[1].takeIf { it.isNotEmpty() }
+ val login = noreply.groupValues[2]
+ return CommitPerson(
+ login = login,
+ avatarUrl =
+ if (id != null) "https://avatars.githubusercontent.com/u/$id?v=4"
+ else "https://github.com/$login.png",
+ profileUrl = "https://github.com/$login",
+ isBot = login.endsWith("[bot]"),
+ )
+ }
+ identities[name.lowercase()]?.let {
+ return it
+ }
+ return resolvePerson(name, freshness)?.let {
+ CommitPerson(
+ login = it.login,
+ avatarUrl = it.avatarUrl,
+ profileUrl = it.profileUrl,
+ isBot = it.isBot,
+ )
+ } ?: CommitPerson(login = name, avatarUrl = null, profileUrl = null)
+ }
+
+ /**
+ * The canary builds, newest first.
+ *
+ * These are **prereleases**, not Actions artifacts, and that is the whole point. GitHub gates an
+ * artifact download behind an account even for a public repository — `actions/artifacts//zip`
+ * answers 401 to an anonymous caller, while a release asset answers 206 — so sourcing canaries
+ * from artifacts meant asking every would-be tester for an OAuth grant to work around a storage
+ * decision. CI attaches the same zips to a rolling `canary-` prerelease, and this
+ * reads that, so nobody signs in to anything.
+ *
+ * Filtered to the canary tag rather than taking every prerelease: a hand-cut release candidate
+ * is also a prerelease, and it is not a nightly.
+ */
+ suspend fun canaryBuilds(freshness: Freshness = Freshness.Revalidate): List =
+ withContext(Dispatchers.IO) {
+ val body =
+ get("$API/$REPO/releases?per_page=$CANARY_FETCH", freshness)
+ ?: return@withContext emptyList()
+
+ runCatching { json.decodeFromString>(body) }
+ .getOrDefault(emptyList())
+ .filter { it.prerelease && it.tagName.startsWith(CANARY_TAG_PREFIX) }
+ .take(CANARY_KEEP)
+ .map { release ->
+ CanaryBuild(
+ id = release.id,
+ title = release.name ?: release.tagName,
+ branch = release.tagName,
+ shortSha = release.targetCommitish.take(7),
+ epochSeconds = parseIso8601(release.publishedAt.orEmpty()),
+ htmlUrl = release.htmlUrl,
+ artifacts =
+ release.assets.map {
+ CanaryArtifact(
+ id = it.id,
+ name = it.name,
+ sizeInBytes = it.size,
+ expired = false,
+ downloadUrl = it.downloadUrl,
+ )
+ },
+ )
+ }
+ }
+
+ /**
+ * Every published build, both channels, newest first.
+ *
+ * One fetch for both because they come from the same endpoint, and because deciding which
+ * channel a reader is on needs to see both: a canary that has aged out of the rolling five is
+ * still recognisable as a canary by being *newer than the newest stable release*, and that
+ * comparison is impossible with only one of the two lists in hand.
+ */
+ suspend fun frameworkReleases(freshness: Freshness = Freshness.Revalidate):
+ List =
+ withContext(Dispatchers.IO) {
+ val body =
+ get("$API/$REPO/releases?per_page=$CANARY_FETCH", freshness)
+ ?: return@withContext emptyList()
+
+ runCatching { json.decodeFromString>(body) }
+ .getOrDefault(emptyList())
+ .mapNotNull { release ->
+ val canary = release.prerelease && release.tagName.startsWith(CANARY_TAG_PREFIX)
+ FrameworkRelease(
+ tag = release.tagName,
+ title = release.name ?: release.tagName,
+ versionCode = release.versionCode() ?: return@mapNotNull null,
+ isCanary = canary,
+ notesMarkdown = release.body,
+ htmlUrl = release.htmlUrl,
+ epochSeconds = parseIso8601(release.publishedAt.orEmpty()),
+ // A branch name is not a build. Only a SHA identifies one.
+ commit =
+ release.targetCommitish.takeIf { c ->
+ c.length >= 7 && c.all { it.isDigit() || it in 'a'..'f' }
+ },
+ zips =
+ release.assets
+ .filter { it.name.endsWith(".zip", ignoreCase = true) }
+ .map {
+ CanaryArtifact(
+ id = it.id,
+ name = it.name,
+ sizeInBytes = it.size,
+ expired = false,
+ downloadUrl = it.downloadUrl,
+ )
+ },
+ )
+ }
+ .sortedByDescending { it.versionCode }
+ }
+
+ /**
+ * The build number a release represents, or null when it is not comparable with ours.
+ *
+ * `canary-3049` states it outright. A stable tag does not, so it is read out of the zip's file
+ * name — the CI names them with the same version code — and a release whose number cannot be
+ * established at all is dropped rather than compared as zero, which would have made every
+ * stable release look older than every canary.
+ *
+ * **Only releases of this product count, and that is not pedantry.** The version code restarted
+ * when LSPosed became Vector: this repository's own release list holds `LSPosed-v1.11.0-7209`
+ * beside `Vector-v2.0-3021`, so a plain numeric comparison makes the *older* project look four
+ * thousand builds newer. Without this filter the manager offered LSPosed 1.11.0 to a Vector
+ * 3049 device and called it an update — a cross-product downgrade, flashed with root. Matching
+ * the asset prefix is what keeps the comparison inside one numbering scheme.
+ */
+ private fun GhRelease.versionCode(): Long? {
+ val ours = assets.filter { it.name.startsWith(ZIP_PREFIX, ignoreCase = true) }
+ if (ours.isEmpty()) return null
+ if (tagName.startsWith(CANARY_TAG_PREFIX)) {
+ return tagName.removePrefix(CANARY_TAG_PREFIX).toLongOrNull()
+ }
+ return ours.firstNotNullOfOrNull { asset ->
+ Regex("(\\d{3,})").findAll(asset.name).map { it.value }.lastOrNull()?.toLongOrNull()
+ }
+ }
+
+ @Serializable
+ private data class ResolvedPerson(
+ val login: String,
+ val avatarUrl: String?,
+ val profileUrl: String?,
+ val isBot: Boolean,
+ )
+
+ /**
+ * Turns a name signed in a trailer into a GitHub account, when it can be done safely.
+ *
+ * GitHub's own web UI resolves these by matching the commit *email* to an account, which it can
+ * do because it holds every address a user has ever verified. We cannot: the address is usually
+ * private, and `search/users?q=…+in:email` finds nothing for it — checked against
+ * `krc440002@gmail.com`, which the web UI resolves and the search API returns zero results for.
+ *
+ * What is left is asking whether an account exists under that name, and that is only safe for
+ * names that are plainly *handles*. `GET /users/Qing` answers 200 with a real account — id
+ * 158244, an unrelated person — so probing every display name would eventually attach a
+ * stranger's face and profile to someone else's contribution. Requiring a digit, a hyphen or an
+ * underscore keeps the shape of a handle (`frknkrc44`) and rejects the shape of a name
+ * (`Qing`, `Furkan Karcıoğlu`).
+ *
+ * The cost of the guard is that a handle made only of letters stays unresolved. That is the
+ * right way to be wrong: an unlinked contributor is merely uncredited, a mislinked one is
+ * credited to the wrong person.
+ */
+ private fun resolvePerson(name: String, freshness: Freshness): ResolvedPerson? {
+ val key = name.lowercase()
+ if (resolvedPeople.containsKey(key)) return resolvedPeople[key]
+ if (!HANDLE_SHAPED.matches(name)) return null
+ // A cache-only load must not decide that a name is unresolvable. The request would be
+ // served FORCE_CACHE, miss, and the miss would be written down as a permanent failure — so
+ // the first launch after install, which reads the feed from disk, would poison every name
+ // before the network was ever asked.
+ if (freshness == Freshness.Cached) return null
+
+ val found =
+ runCatching {
+ get("$API_ROOT/users/$name", freshness)?.let {
+ val user = json.decodeFromString(it)
+ ResolvedPerson(
+ login = user.login,
+ avatarUrl = user.avatarUrl,
+ profileUrl = user.htmlUrl,
+ isBot = user.type == "Bot" || user.login.endsWith("[bot]"),
+ )
+ }
+ }
+ .getOrNull()
+
+ resolvedPeople[key] = found
+ runCatching { peopleFile.writeText(json.encodeToString(resolvedPeople.toMap())) }
+ return found
+ }
+
+ private fun prNumber(subject: String): Int? =
+ PR_SUFFIX.find(subject)?.groupValues?.getOrNull(1)?.toIntOrNull()
+
+ private fun iso8601(epochSeconds: Long): String {
+ val cal =
+ java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")).apply {
+ timeInMillis = epochSeconds * 1000
+ }
+ return String.format(
+ java.util.Locale.ROOT,
+ "%04d-%02d-%02dT%02d:%02d:%02dZ",
+ cal.get(java.util.Calendar.YEAR),
+ cal.get(java.util.Calendar.MONTH) + 1,
+ cal.get(java.util.Calendar.DAY_OF_MONTH),
+ cal.get(java.util.Calendar.HOUR_OF_DAY),
+ cal.get(java.util.Calendar.MINUTE),
+ cal.get(java.util.Calendar.SECOND),
+ )
+ }
+
+ private fun parseIso8601(value: String): Long =
+ runCatching {
+ val f =
+ java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.ROOT)
+ .apply { timeZone = java.util.TimeZone.getTimeZone("UTC") }
+ (f.parse(value)?.time ?: 0L) / 1000
+ }
+ .getOrDefault(0L)
+
+ companion object {
+ const val OWNER = "JingMatrix"
+ const val REPO = "$OWNER/Vector"
+ const val REPO_URL = "https://github.com/$REPO"
+ const val ISSUES_URL = "$REPO_URL/issues"
+ const val PULLS_URL = "$REPO_URL/pulls"
+ const val DISCUSSIONS_URL = "$REPO_URL/discussions"
+ const val GOOD_FIRST_ISSUE_URL = "$REPO_URL/issues?q=is%3Aopen+label%3A%22good+first+issue%22"
+
+ /**
+ * Canary builds come off CI rather than a release tag, so testing one means going to the
+ * workflow's run list and taking the artifact from the most recent master build.
+ */
+ const val CANARY_URL = "$REPO_URL/actions/workflows/core.yml?query=branch%3Amaster"
+ private const val CANARY_TAG_PREFIX = "canary-"
+
+ /**
+ * What this product's own release zips are called.
+ *
+ * The release list still carries the pre-rename LSPosed builds, whose version codes are
+ * from a different and higher numbering; see `versionCode()`.
+ */
+ private const val ZIP_PREFIX = "Vector-"
+
+ /** CI keeps five; a few extra are fetched so a stable release among them costs nothing. */
+ private const val CANARY_FETCH = 12
+ private const val CANARY_KEEP = 5
+
+ private const val API = "https://api.github.com/repos"
+ private const val API_ROOT = "https://api.github.com"
+
+ /**
+ * A name shaped like a handle rather than a display name.
+ *
+ * A digit, a hyphen or an underscore somewhere in it, and nothing that a GitHub login
+ * cannot contain. See [resolvePerson] for why the bar is deliberately this high.
+ */
+ private val HANDLE_SHAPED =
+ Regex("^(?=.*[0-9_-])[A-Za-z0-9](?:[A-Za-z0-9_]|-(?=[A-Za-z0-9_])){0,38}$")
+
+ /**
+ * Six months: long enough that a quiet stretch does not read as a dead project, short
+ * enough that the people row is still a scoreboard rather than a monument. At this
+ * project's rate that is ~44 commits, which is one unpaginated request. Overridable in
+ * settings.
+ */
+ const val DEFAULT_WINDOW_MONTHS = 6
+
+ private const val DAYS_PER_MONTH = 30L
+
+ private const val REVALIDATE_MINUTES = 30L
+
+ private val PR_SUFFIX = Regex("""\(#(\d+)\)\s*$""")
+
+ private val LAST_PAGE = Regex("""[?&]page=(\d+)>;\s*rel="last"""")
+
+ private val CO_AUTHOR =
+ Regex("""(?im)^\s*Co-authored-by:\s*(.+?)\s*<([^>]+)>\s*$""")
+
+ private val GITHUB_NOREPLY =
+ Regex("""^(?:(\d+)\+)?([^@]+)@users\.noreply\.github\.com$""", RegexOption.IGNORE_CASE)
+ }
+}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogFile.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogFile.kt
new file mode 100644
index 000000000..52691b861
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogFile.kt
@@ -0,0 +1,380 @@
+package org.matrix.vector.manager.data.log
+
+import android.os.ParcelFileDescriptor
+import java.io.Closeable
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+import kotlin.math.max
+import kotlin.math.min
+import kotlinx.coroutines.yield
+
+/**
+ * A random-access window onto one of the daemon's log files.
+ *
+ * The screen this feeds used to call `readLines()` and hold the result in a `StateFlow` — for
+ * *both* tabs, on every refresh, whether or not the tab was visible. That is roughly 13 MB of
+ * `String` churn per refresh inside a process whose heap belongs to `com.android.shell`.
+ *
+ * The honest correction to the usual telling of that story is that a single `getLog()` is capped
+ * today: `logcat.cpp` rotates at 4 MB per part, and a rotated part on a real device measures
+ * 4,194,639 bytes. So the old reader was wasteful rather than instantly fatal. It still had to go,
+ * because the cap is a property of the daemon build that happens to be running — rotation only
+ * fires inside the logd read loop, so a wedged logd grows the part without bound — and the manager
+ * cannot inspect the provenance of the descriptor it was handed before it reads from it. **A
+ * reader whose peak memory scales with file size is wrong regardless of today's number.**
+ *
+ * So nothing here scales with the file:
+ * - [index] never allocates a `String`. It scans bytes for `'\n'` and records line offsets into a
+ * `LongArray` — 8 bytes per line, ~240 KB for a full part, and capped at [MAX_INDEXED_LINES].
+ * - [readRows] materialises at most a window's worth of lines, reading them in one seek per
+ * 256 KB block. Peak heap is a function of the window size alone.
+ * - [scan] streams the whole file to build a filter, but only ever holds one block plus the
+ * matching *offsets*.
+ *
+ * The descriptor is real and seekable: `ManagerService.getVerboseLog()` opens
+ * `/proc/self/fd/N`, which resolves the procfs symlink back to the log inode, so positional reads
+ * work and this class exploits them rather than streaming forward.
+ *
+ * Ownership is exactly one object. [ParcelFileDescriptor.AutoCloseInputStream] adopts the
+ * descriptor and closes it once, in [close]. The previous code wrapped the raw
+ * `pfd.fileDescriptor` in a `FileInputStream` *and* closed the `ParcelFileDescriptor` in a
+ * `finally`, closing the same fd number twice — and between the two closes the runtime is free to
+ * hand that number to an OkHttp socket or a Coil bitmap, which the second close then silently
+ * detaches.
+ */
+class LogFile(pfd: ParcelFileDescriptor) : Closeable {
+
+ private val stream = ParcelFileDescriptor.AutoCloseInputStream(pfd)
+ private val channel: FileChannel = stream.channel
+ private val block = ByteArray(READ_BLOCK)
+ private val oneByte = ByteBuffer.allocate(1)
+
+ /**
+ * Pass one: where every line starts.
+ *
+ * One sequential read of the page cache with a tight byte loop and no decoding at all. The
+ * size is captured once and everything past it is ignored, so a line the daemon is appending
+ * while this runs is never half-decoded — it simply appears on the next refresh.
+ */
+ suspend fun index(): LogIndex {
+ val size = channel.size()
+ val starts = LongVec()
+ starts.add(0L)
+ var dropped = 0
+ var pos = 0L
+
+ while (pos < size) {
+ val want = min(READ_BLOCK.toLong(), size - pos).toInt()
+ val read = readAt(pos, want)
+ if (read <= 0) break
+ for (i in 0 until read) {
+ if (block[i] == NEWLINE) starts.add(pos + i + 1)
+ }
+ pos += read
+
+ // A file long enough to blow the offset table is a file nobody is going to read from
+ // the top anyway, so the leading offsets are dropped and the header says so. Silently
+ // showing a truncated file as if it were whole is the one thing not allowed.
+ if (starts.size > MAX_INDEXED_LINES + DROP_BLOCK) {
+ starts.dropFirst(DROP_BLOCK)
+ dropped += DROP_BLOCK
+ }
+ yield()
+ }
+
+ // The array doubles as its own end sentinel: line k spans [bounds[k], bounds[k + 1]).
+ if (starts.last() != size) starts.add(size)
+ return LogIndex(starts.toArray(), dropped)
+ }
+
+ /**
+ * Pass two: turn a selection of lines into rows.
+ *
+ * [lines] is ascending and absolute. The unfiltered case passes a contiguous run, so the read
+ * covers exactly the bytes needed; a filtered case passes a sparse selection, and the block
+ * loop below reads through the gaps and discards them rather than issuing one seek per line.
+ * Either way the bytes held at once are bounded by [READ_BLOCK].
+ */
+ suspend fun readRows(index: LogIndex, lines: IntArray): List {
+ val rows = ArrayList(lines.size + 8)
+ var lastDate: String? = null
+ var traceOwner = -1
+ var trace: ArrayList? = null
+
+ fun flushTrace() {
+ val frames = trace
+ if (traceOwner >= 0 && frames != null) {
+ rows[traceOwner] = (rows[traceOwner] as LogRow.Entry).copy(trace = frames)
+ }
+ trace = null
+ traceOwner = -1
+ }
+
+ forEachLine(index, lines, null) { lineIndex, text, truncated ->
+ if (traceOwner >= 0 && isContinuationLine(text)) {
+ // A multi-line message reaches the file as one writev, so its continuation lines
+ // carry no prefix. They are frames of the entry above, not entries of their own.
+ (trace ?: ArrayList(8).also { trace = it }).add(text)
+ } else {
+ flushTrace()
+ when (val row = parseLogLine(lineIndex, text, truncated)) {
+ is LogRow.Entry -> {
+ if (row.date != lastDate) {
+ lastDate = row.date
+ rows.add(LogRow.DayBreak(lineIndex, row.date))
+ }
+ rows.add(row)
+ traceOwner = rows.size - 1
+ }
+ else -> rows.add(row)
+ }
+ }
+ }
+ flushTrace()
+ return rows
+ }
+
+ /**
+ * Walks back from [line] to the entry that owns it.
+ *
+ * Without this a window boundary landing between an entry and its stack trace opens the page
+ * on orphan frames with nothing to attach them to. Only the first byte of each candidate line
+ * is read — up to [TRACE_LOOKBACK] single-byte positional reads, all of them page-cache hits.
+ */
+ fun entryStart(index: LogIndex, line: Int): Int {
+ if (line >= index.lineCount) return line
+ var at = line
+ var steps = 0
+ while (at > 0 && steps < TRACE_LOOKBACK) {
+ val first = firstByte(index, at)
+ if (first != SPACE && first != TAB) break
+ at--
+ steps++
+ }
+ return at
+ }
+
+ /**
+ * Builds the filter, and the facets, in one streaming pass.
+ *
+ * Only a *matching* line is ever kept, and only as its offset, so filtering a 40 MB file costs
+ * one sequential read and an `IntArray` of hits. Progress is a real fraction of bytes scanned
+ * rather than a spinner, because on a large file this is long enough to be worth reporting
+ * honestly.
+ */
+ suspend fun scan(
+ index: LogIndex,
+ query: LogQuery,
+ onProgress: (Float) -> Unit,
+ ): LogScanResult {
+ val matches = if (query.isActive) IntVec() else null
+ val tags = HashMap()
+ val levels = HashMap()
+ var previousMatched = false
+
+ forEachLine(index, null, onProgress) { lineIndex, text, truncated ->
+ if (isContinuationLine(text)) {
+ // Frames follow their entry into the filtered view; a stack trace whose header
+ // matched and whose body vanished is a filter actively hiding the answer.
+ if (previousMatched) matches?.add(lineIndex)
+ return@forEachLine
+ }
+ val row = parseLogLine(lineIndex, text, truncated)
+ if (row is LogRow.Entry) {
+ tags[row.tag] = (tags[row.tag] ?: 0) + 1
+ levels[row.level] = (levels[row.level] ?: 0) + 1
+ }
+ previousMatched = query.matches(row)
+ if (previousMatched) matches?.add(lineIndex)
+ }
+
+ return LogScanResult(
+ matches = matches?.toArray(),
+ facets =
+ LogFacets(
+ tags = tags.entries.sortedByDescending { it.value }.map { it.key to it.value },
+ levels = levels,
+ ),
+ )
+ }
+
+ override fun close() {
+ runCatching { stream.close() }
+ }
+
+ // --- Block iteration ---------------------------------------------------------------------
+
+ /**
+ * Feeds lines to [action] a block at a time.
+ *
+ * Blocks end on a line boundary, so no line ever straddles two reads and the caller never has
+ * to stitch. A single line longer than [READ_BLOCK] is the one exception and is cut, which is
+ * also where [MAX_LINE_CHARS] would have cut it anyway.
+ */
+ private suspend fun forEachLine(
+ index: LogIndex,
+ selection: IntArray?,
+ onProgress: ((Float) -> Unit)?,
+ action: (lineIndex: Int, text: String, truncated: Boolean) -> Unit,
+ ) {
+ val bounds = index.bounds
+ val count = selection?.size ?: index.lineCount
+ if (count == 0) return
+ val span = (bounds[index.lineCount] - bounds[0]).coerceAtLeast(1L)
+
+ var k = 0
+ while (k < count) {
+ val startLine = selection?.get(k) ?: k
+ val startOffset = bounds[startLine]
+
+ // Take as many whole lines as fit in one block, always at least one.
+ var endLine = startLine + 1
+ while (endLine < index.lineCount && bounds[endLine + 1] - startOffset <= READ_BLOCK) {
+ endLine++
+ }
+ val want = min(bounds[endLine] - startOffset, READ_BLOCK.toLong()).toInt()
+ val read = readAt(startOffset, want)
+
+ while (k < count) {
+ val line = selection?.get(k) ?: k
+ if (line >= endLine) break
+ val from = (bounds[line] - startOffset).toInt()
+ val to = min((bounds[line + 1] - startOffset).toInt(), read)
+ var length = max(0, to - from)
+ // The stored bound includes the newline that ended the line.
+ if (length > 0 && block[from + length - 1] == NEWLINE) length--
+ if (length > 0 && block[from + length - 1] == RETURN) length--
+ val cut = length > MAX_LINE_CHARS
+ if (cut) length = MAX_LINE_CHARS
+ action(line, String(block, from, length, Charsets.UTF_8), cut)
+ k++
+ }
+
+ onProgress?.invoke(((bounds[endLine] - bounds[0]).toFloat() / span).coerceIn(0f, 1f))
+ yield()
+ }
+ }
+
+ /** Fills [block] from [offset]; returns how many bytes actually landed. */
+ private fun readAt(offset: Long, length: Int): Int {
+ val buffer = ByteBuffer.wrap(block, 0, length)
+ var total = 0
+ while (buffer.hasRemaining()) {
+ val n = channel.read(buffer, offset + total)
+ if (n <= 0) break
+ total += n
+ }
+ return total
+ }
+
+ private fun firstByte(index: LogIndex, line: Int): Int {
+ if (index.bounds[line + 1] <= index.bounds[line]) return -1
+ oneByte.clear()
+ if (channel.read(oneByte, index.bounds[line]) <= 0) return -1
+ return oneByte.get(0).toInt()
+ }
+
+ companion object {
+ /** One page-cache-friendly read. Also the largest amount of raw log held at any moment. */
+ private const val READ_BLOCK = 256 * 1024
+
+ /**
+ * 400,000 lines is ~3.2 MB of offsets, and about ten times the largest log the daemon can
+ * currently produce. Past it the *oldest* lines are dropped, because a log is read from
+ * the end.
+ */
+ private const val MAX_INDEXED_LINES = 400_000
+
+ private const val DROP_BLOCK = 50_000
+
+ /** How far back a window start may walk to find the entry that owns a stack frame. */
+ private const val TRACE_LOOKBACK = 64
+
+ private const val NEWLINE = '\n'.code.toByte()
+ private const val RETURN = '\r'.code.toByte()
+ private const val SPACE = ' '.code
+ private const val TAB = '\t'.code
+ }
+}
+
+/**
+ * Where every line of the file starts, plus the end sentinel.
+ *
+ * `bounds` has `lineCount + 1` entries; line `k` is the bytes in `[bounds[k], bounds[k + 1])`.
+ * [droppedLeading] is how many lines fell off the front of an over-long file, and exists so the
+ * header can say so rather than quietly misreport the file's length.
+ */
+class LogIndex(val bounds: LongArray, val droppedLeading: Int) {
+ val lineCount: Int
+ get() = bounds.size - 1
+}
+
+/** What [LogFile.scan] found: the filtered line numbers, and what the file contains. */
+class LogScanResult(val matches: IntArray?, val facets: LogFacets)
+
+/** The tags and levels actually present, with counts, so the filter sheet cannot go stale. */
+data class LogFacets(
+ val tags: List> = emptyList(),
+ val levels: Map = emptyMap(),
+)
+
+/** Everything that narrows the view. All of it is applied in one pass over the file. */
+data class LogQuery(
+ val levels: Set = emptySet(),
+ val tag: String? = null,
+ val text: String = "",
+) {
+ val isActive: Boolean
+ get() = levels.isNotEmpty() || tag != null || text.isNotBlank()
+
+ fun matches(row: LogRow): Boolean =
+ when (row) {
+ is LogRow.Entry ->
+ (levels.isEmpty() || row.level in levels) &&
+ (tag == null || row.tag == tag) &&
+ (text.isBlank() ||
+ row.message.contains(text, ignoreCase = true) ||
+ row.tag.contains(text, ignoreCase = true))
+ // A rotation banner has neither level nor tag, so it survives only a plain text
+ // search. It marks where the daemon restarted, which is worth keeping when it can be.
+ is LogRow.Marker ->
+ levels.isEmpty() &&
+ tag == null &&
+ (text.isBlank() || row.text.contains(text, ignoreCase = true))
+ is LogRow.DayBreak -> false
+ }
+}
+
+/** Growable `long` storage. `ArrayList` would box every offset. */
+private class LongVec(initial: Int = 1 shl 12) {
+ private var data = LongArray(initial)
+ var size = 0
+ private set
+
+ fun add(value: Long) {
+ if (size == data.size) data = data.copyOf(size * 2)
+ data[size++] = value
+ }
+
+ fun last(): Long = if (size == 0) -1L else data[size - 1]
+
+ fun dropFirst(n: Int) {
+ System.arraycopy(data, n, data, 0, size - n)
+ size -= n
+ }
+
+ fun toArray(): LongArray = data.copyOf(size)
+}
+
+/** The same, for line numbers, which are half the width. */
+private class IntVec(initial: Int = 1 shl 10) {
+ private var data = IntArray(initial)
+ private var size = 0
+
+ fun add(value: Int) {
+ if (size == data.size) data = data.copyOf(size * 2)
+ data[size++] = value
+ }
+
+ fun toArray(): IntArray = data.copyOf(size)
+}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogModel.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogModel.kt
new file mode 100644
index 000000000..0db0e170c
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogModel.kt
@@ -0,0 +1,212 @@
+package org.matrix.vector.manager.data.log
+
+/**
+ * The shape of a line in the daemon's log, and the scanner that recovers it.
+ *
+ * The authority for this format is not a sample of the output — it is the writer,
+ * `daemon/src/main/jni/logcat.cpp`, which emits every entry as a single `writev` of
+ *
+ * ```
+ * "[ " %Y-%m-%dT%H:%M:%S ".%03ld %8d:%6d:%6d %c/%-15.*s ] " "\n"
+ * ```
+ *
+ * Three properties of that format decide how this parser is written, and guessing any of them
+ * wrong is how log parsers rot:
+ *
+ * 1. **The widths are `printf` minimums, not columns.** A uid of `1010324` is seven digits and a
+ * future pid can exceed six, so the prefix has to be *scanned*. Slicing at constant offsets
+ * works right up until the day it silently does not.
+ * 2. **A message containing newlines is still one `writev`.** Its continuation lines therefore
+ * carry no prefix at all — a Java stack trace arrives as one entry followed by N raw lines each
+ * beginning with a tab. Those frames belong to the entry above them, not to nothing.
+ * 3. **Not every line is an entry.** `----part N start----` / `-----part N end----` mark the
+ * daemon rotating to a fresh file, and the watchdog writes its own banners. Those are real
+ * information about the log rather than noise, so they survive as [LogRow.Marker] instead of
+ * being dropped.
+ *
+ * Anything the scanner cannot make sense of degrades to a marker carrying the raw text. A log
+ * viewer that hides a line it failed to understand is worse than useless during a diagnosis.
+ */
+enum class LogLevel(val char: Char) {
+ VERBOSE('V'),
+ DEBUG('D'),
+ INFO('I'),
+ WARN('W'),
+ ERROR('E'),
+ FATAL('F'),
+ SILENT('S'),
+ UNKNOWN('?');
+
+ companion object {
+ /** `kLogChar` in logcat.cpp, in the same order Android's priorities are numbered. */
+ fun of(c: Char): LogLevel =
+ when (c) {
+ 'V' -> VERBOSE
+ 'D' -> DEBUG
+ 'I' -> INFO
+ 'W' -> WARN
+ 'E' -> ERROR
+ 'F' -> FATAL
+ 'S' -> SILENT
+ else -> UNKNOWN
+ }
+
+ /** The levels worth offering as a filter; `SILENT` and `UNKNOWN` never reach a reader. */
+ val selectable = listOf(VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL)
+ }
+}
+
+/** One row of the rendered log. [index] is the line's absolute position in the file. */
+sealed interface LogRow {
+ val index: Int
+
+ /**
+ * Stable identity for the lazy list.
+ *
+ * This is what lets the window be extended upwards without the viewport lurching: the list
+ * re-resolves its first visible item by key after rows are inserted above it, so a prepend
+ * re-anchors instead of shifting. A day break shares its line's index with the entry it
+ * introduces, so its key is negated to keep the two distinct.
+ */
+ val key: Long
+ get() = index.toLong()
+
+ data class Entry(
+ override val index: Int,
+ /** `yyyy-MM-dd`, kept as written; only the day separator ever needs it. */
+ val date: String,
+ /** `HH:mm:ss.SSS`. The date is redundant on every row and moves to the separator. */
+ val time: String,
+ val uid: Int,
+ val pid: Int,
+ val tid: Int,
+ val level: LogLevel,
+ val tag: String,
+ val message: String,
+ /** Continuation lines of a multi-line message — in practice, stack frames. */
+ val trace: List = emptyList(),
+ /** Set when the line exceeded [MAX_LINE_CHARS] and was cut. */
+ val truncated: Boolean = false,
+ ) : LogRow
+
+ /** A rotation banner, a watchdog line, or anything the scanner could not read. */
+ data class Marker(override val index: Int, val text: String) : LogRow
+
+ /** Synthetic: introduces the first entry of a calendar day. */
+ data class DayBreak(override val index: Int, val date: String) : LogRow {
+ override val key: Long
+ get() = -(index.toLong() + 1)
+ }
+}
+
+/**
+ * Cut point for a single line.
+ *
+ * The longest line observed in either log on a real device is 816 characters (an attestation
+ * dump), so this only ever bites on pathological output — but without it one runaway line sets
+ * the horizontal extent for the entire list and makes panning useless.
+ */
+const val MAX_LINE_CHARS = 4096
+
+/** The three-character delimiter that ends the prefix. See [parseLogLine]. */
+private const val DELIMITER = " ] "
+
+/** `"[ "` + 23 characters of timestamp + at least `" 0: 0: 0 V/x ] "`. */
+private const val MIN_PREFIX = 26
+
+/** A line is a continuation of the entry above it when it starts with whitespace. */
+fun isContinuationLine(text: String): Boolean =
+ text.isNotEmpty() && (text[0] == ' ' || text[0] == '\t')
+
+/** Parses one raw line, degrading to [LogRow.Marker] rather than failing. */
+fun parseLogLine(index: Int, text: String, truncated: Boolean = false): LogRow =
+ parseEntry(index, text, truncated) ?: LogRow.Marker(index, text)
+
+private fun parseEntry(index: Int, line: String, truncated: Boolean): LogRow.Entry? {
+ val n = line.length
+ if (n < MIN_PREFIX || line[0] != '[' || line[1] != ' ') return null
+
+ // The timestamp is fixed-width, so it is the one part worth checking by position: cheap
+ // separators to reject in six comparisons before any digit scanning happens.
+ if (
+ line[6] != '-' ||
+ line[9] != '-' ||
+ line[12] != 'T' ||
+ line[15] != ':' ||
+ line[18] != ':' ||
+ line[21] != '.'
+ )
+ return null
+
+ var i = 25 // "[ " + 23 characters of timestamp
+
+ val uidField = readInt(line, skipSpaces(line, i))
+ if (uidField == NO_INT) return null
+ i = endOf(uidField)
+ if (i >= n || line[i] != ':') return null
+
+ val pidField = readInt(line, skipSpaces(line, i + 1))
+ if (pidField == NO_INT) return null
+ i = endOf(pidField)
+ if (i >= n || line[i] != ':') return null
+
+ val tidField = readInt(line, skipSpaces(line, i + 1))
+ if (tidField == NO_INT) return null
+ i = endOf(tidField)
+
+ if (i + 2 >= n || line[i] != ' ' || line[i + 2] != '/') return null
+ val level = LogLevel.of(line[i + 1])
+ val tagStart = i + 3
+
+ // The delimiter is the three-character sequence, not a bare ']'. A message that contains a
+ // bracket — "[TX_ID: 773] Intercept…" — has no space before its ']', and the tag is padded
+ // with spaces to fifteen columns, so the first " ] " is always the real end of the prefix.
+ val delimiter = line.indexOf(DELIMITER, tagStart)
+ if (delimiter < 0) return null
+
+ return LogRow.Entry(
+ index = index,
+ date = line.substring(2, 12),
+ time = line.substring(13, 25),
+ uid = valueOf(uidField),
+ pid = valueOf(pidField),
+ tid = valueOf(tidField),
+ level = level,
+ tag = line.substring(tagStart, delimiter).trimEnd(),
+ message = line.substring(delimiter + DELIMITER.length),
+ truncated = truncated,
+ )
+}
+
+private fun skipSpaces(s: String, from: Int): Int {
+ var i = from
+ while (i < s.length && s[i] == ' ') i++
+ return i
+}
+
+/**
+ * [readInt] has to return both the value and where it stopped.
+ *
+ * The two are packed into one `Long` rather than returned as a `Pair`, because a `Pair` would
+ * allocate three times per parsed line — and a window of a full 4 MB log part is thirty thousand
+ * lines, re-parsed every time the window moves. A top-level scratch variable would be shorter
+ * still, but both log panes parse concurrently on the IO pool and would corrupt each other.
+ */
+private const val NO_INT = -1L
+
+private fun valueOf(field: Long): Int = (field ushr 32).toInt()
+
+private fun endOf(field: Long): Int = (field and 0xFFFFFFFFL).toInt()
+
+/** Reads an unsigned decimal, refusing anything long enough to overflow. */
+private fun readInt(s: String, from: Int): Long {
+ var i = from
+ var value = 0L
+ while (i < s.length && s[i] in '0'..'9') {
+ value = value * 10 + (s[i] - '0')
+ if (value > Int.MAX_VALUE) return NO_INT
+ i++
+ }
+ if (i == from) return NO_INT
+ return (value shl 32) or i.toLong()
+}
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/AppInfo.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/AppInfo.kt
new file mode 100644
index 000000000..c32c51894
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/AppInfo.kt
@@ -0,0 +1,19 @@
+package org.matrix.vector.manager.data.model
+
+import android.content.pm.ApplicationInfo
+
+/** Represents an installed application for the Scope configuration screen. */
+data class AppInfo(
+ val packageName: String,
+ val userId: Int,
+ val appName: String,
+ val isSystemApp: Boolean,
+ val isGame: Boolean,
+ val isSelectedInScope: Boolean,
+ val isRecommended: Boolean,
+ /** When the package was last installed or updated, for the "recently updated" sort. */
+ val lastUpdateTime: Long,
+ /** When it was first installed — a different question, and the legacy UI sorted on both. */
+ val firstInstallTime: Long,
+ val applicationInfo: ApplicationInfo,
+)
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/InstalledModule.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/InstalledModule.kt
new file mode 100644
index 000000000..4b2a00b79
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/InstalledModule.kt
@@ -0,0 +1,22 @@
+package org.matrix.vector.manager.data.model
+
+import android.content.pm.ApplicationInfo
+
+/** Pure Kotlin data class representing an installed Xposed module. */
+data class InstalledModule(
+ val packageName: String,
+ val userId: Int,
+ val appName: String,
+ val versionName: String,
+ val versionCode: Long,
+ val description: String,
+ val minVersion: Int,
+ val targetVersion: Int,
+ val isLegacy: Boolean,
+ /** False when the module declares no Xposed API at all, which the old manager flagged. */
+ val declaresApiVersion: Boolean,
+ /** When the module was last installed or updated — how you find the one you just added. */
+ val lastUpdateTime: Long,
+ val isEnabled: Boolean,
+ val applicationInfo: ApplicationInfo, // Kept for Icon loading via Coil/Glide later
+)
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetection.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetection.kt
new file mode 100644
index 000000000..4cd4271c9
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetection.kt
@@ -0,0 +1,202 @@
+package org.matrix.vector.manager.data.model
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import java.util.Properties
+import java.util.zip.ZipFile
+
+/**
+ * Everything the manager can learn about a package by looking at it.
+ *
+ * There are two generations of Xposed module and both must be recognised, which is the bug this
+ * file exists to fix: the previous implementation tested only the legacy `xposedmodule` /
+ * `xposedminversion` manifest meta-data, so every module written against API 100 and later — which
+ * ships no such meta-data, only files inside its APK — was invisible in the module list.
+ *
+ * The whole inspection is done in **one pass over the APK**. Opening the zip is the expensive part
+ * and the list opens it once per module already; doing it three times to answer three questions
+ * would triple the cost of showing the screen.
+ */
+data class ModuleManifest(
+ val isModule: Boolean = false,
+ val isLegacy: Boolean = false,
+ /** The Xposed API the module needs, or 0 when it does not say. */
+ val minApiVersion: Int = 0,
+ val targetApiVersion: Int = 0,
+ /** Packages the module asks to hook. */
+ val scope: List = emptyList(),
+ /** True when [scope] is the whole of it and the user cannot widen it. */
+ val staticScope: Boolean = false,
+ /** The module's own description, which the two generations store in different places. */
+ val description: String = "",
+) {
+ val declaresApiVersion: Boolean
+ get() = minApiVersion > 0
+}
+
+object ModuleDetection {
+
+ private const val MODERN_ENTRY = "META-INF/xposed/java_init.list"
+ private const val SCOPE_ENTRY = "META-INF/xposed/scope.list"
+ private const val MODULE_PROP = "META-INF/xposed/module.prop"
+ private const val LEGACY_MIN_VERSION = "xposedminversion"
+ private const val LEGACY_SCOPE = "xposedscope"
+ private const val LEGACY_DESCRIPTION = "xposeddescription"
+
+ /**
+ * Reads a package's module metadata, opening each APK at most once.
+ *
+ * Returns [ModuleManifest] with `isModule = false` for anything that is not a module, so a
+ * caller can filter and inspect in a single step.
+ */
+ fun inspect(info: ApplicationInfo, packageManager: PackageManager): ModuleManifest {
+ val legacy = info.metaData?.containsKey(LEGACY_MIN_VERSION) == true
+
+ val apks = buildList {
+ info.splitSourceDirs?.let { addAll(it) }
+ info.sourceDir?.let { add(it) }
+ }
+
+ for (apk in apks) {
+ val modern =
+ runCatching {
+ ZipFile(apk).use { zip ->
+ if (zip.getEntry(MODERN_ENTRY) == null) return@use null
+
+ var minApi = 0
+ var targetApi = 0
+ var static = false
+ zip.getEntry(MODULE_PROP)?.let { entry ->
+ // A malformed module.prop must cost these fields, not the whole
+ // module — Properties.load throws on a bad unicode escape.
+ runCatching {
+ val props =
+ Properties().apply { load(zip.getInputStream(entry)) }
+ minApi = props.getProperty("minApiVersion").toIntOrZero()
+ targetApi = props.getProperty("targetApiVersion").toIntOrZero()
+ static = props.getProperty("staticScope") == "true"
+ }
+ }
+
+ val scope =
+ zip.getEntry(SCOPE_ENTRY)?.let { entry ->
+ zip.getInputStream(entry)
+ .bufferedReader()
+ .readLines()
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ } ?: emptyList()
+
+ ModuleManifest(
+ isModule = true,
+ isLegacy = false,
+ minApiVersion = minApi,
+ targetApiVersion = targetApi,
+ scope = scope,
+ staticScope = static,
+ // A modern module uses the ordinary manifest description.
+ description = info.loadDescription(packageManager)?.toString()?.trim().orEmpty(),
+ )
+ }
+ }
+ .getOrNull()
+ if (modern != null) return modern
+ }
+
+ if (!legacy) return ModuleManifest(isModule = false)
+
+ return ModuleManifest(
+ isModule = true,
+ isLegacy = true,
+ minApiVersion = legacyMinApiVersion(info),
+ scope = legacyScope(info, packageManager),
+ staticScope = false,
+ description = legacyDescription(info, packageManager),
+ )
+ }
+
+ /**
+ * A legacy module's description lives in `xposeddescription`, not `android:description`.
+ *
+ * Reading the manifest attribute for both generations is why every legacy module in the list
+ * showed a blank line where its description should be — legacy modules simply do not set it.
+ * The value is either a literal string or a string-resource id.
+ */
+ private fun legacyDescription(info: ApplicationInfo, packageManager: PackageManager): String {
+ val raw = info.metaData?.get(LEGACY_DESCRIPTION) ?: return ""
+ return when (raw) {
+ is String -> raw.trim()
+ is Int ->
+ runCatching {
+ if (raw == 0) ""
+ else packageManager.getResourcesForApplication(info).getString(raw).trim()
+ }
+ .getOrDefault("")
+ else -> ""
+ }
+ }
+
+ /** The `xposedminversion` a legacy module asks for, or 0 when it does not say. */
+ private fun legacyMinApiVersion(info: ApplicationInfo): Int {
+ val meta = info.metaData ?: return 0
+ // Sometimes an int, sometimes a string like "93 (for Android 9)", so leading digits win.
+ meta.getInt(LEGACY_MIN_VERSION, -1).let { if (it >= 0) return it }
+ val text = meta.getString(LEGACY_MIN_VERSION) ?: return 0
+ return text.trim().takeWhile { it.isDigit() }.toIntOrNull() ?: 0
+ }
+
+ /**
+ * A legacy module's `xposedscope`: either a string-array resource id or a `;`-separated list.
+ */
+ private fun legacyScope(info: ApplicationInfo, packageManager: PackageManager): List {
+ val meta = info.metaData ?: return emptyList()
+ val raw =
+ runCatching {
+ val resourceId = meta.getInt(LEGACY_SCOPE, 0)
+ if (resourceId != 0) {
+ packageManager
+ .getResourcesForApplication(info)
+ .getStringArray(resourceId)
+ .toList()
+ } else {
+ meta.getString(LEGACY_SCOPE)?.split(';')?.map { it.trim() }
+ }
+ }
+ .getOrNull()
+ ?.filter { it.isNotEmpty() } ?: return emptyList()
+
+ // Legacy modules name the system server the other way round. The swap dates to a 2015
+ // XposedBridge commit and every module written since has followed it.
+ return raw.map {
+ when (it) {
+ "android" -> "system"
+ "system" -> "android"
+ else -> it
+ }
+ }
+ }
+
+ private fun String?.toIntOrZero(): Int =
+ this?.trim()?.takeWhile { it.isDigit() }?.toIntOrNull() ?: 0
+}
+
+/**
+ * What a module says it wants to hook.
+ *
+ * [staticScope] means the module only ever applies to [packages] and the user cannot widen it —
+ * the scope editor shows the list read-only rather than pretending the choice is theirs.
+ */
+data class RecommendedScope(val packages: List, val staticScope: Boolean) {
+ val isEmpty: Boolean
+ get() = packages.isEmpty()
+
+ companion object {
+ val NONE = RecommendedScope(emptyList(), staticScope = false)
+ }
+}
+
+/** User ids are encoded into the uid; this is AOSP's `UserHandle.PER_USER_RANGE`. */
+const val PER_USER_RANGE = 100_000
+
+/** `PackageManager.MATCH_ANY_USER`, which is a hidden constant on the public SDK. */
+const val MATCH_ANY_USER = 0x00400000
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetectionCache.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetectionCache.kt
new file mode 100644
index 000000000..9e0951265
Binary files /dev/null and b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/ModuleDetectionCache.kt differ
diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/model/RepoModels.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/RepoModels.kt
new file mode 100644
index 000000000..f4413b75b
--- /dev/null
+++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/model/RepoModels.kt
@@ -0,0 +1,204 @@
+package org.matrix.vector.manager.data.model
+
+import com.google.gson.annotations.SerializedName
+
+/**
+ * The module repository's JSON, as types.
+ *
+ * These mirror what the server actually sends, checked against a live `modules.json` (809 entries)
+ * rather than against the legacy Java model, which had drifted. Two things shape the file:
+ *
+ * **Nullability is not decoration here.** `scope` is null on 506 of the 809 entries, `sourceUrl` on
+ * 369 and `summary` on 121. Gson constructs through `Unsafe` and runs neither Kotlin's
+ * default-argument logic nor its null checks, so a non-null type on a field the server omits yields
+ * a `null` that only explodes at the first dereference, far from the parse. Every field the payload
+ * does not guarantee is therefore declared optional.
+ *
+ * **The list payload is nearly a detail payload.** Each list entry already carries exactly one
+ * release — the newest — with its `.apk` asset and download URL. Installing the current version of
+ * a module needs no second request, which is what lets the Store work on a bad connection.
+ */
+data class OnlineModule(
+ @SerializedName("name") val name: String,
+ @SerializedName("description") val description: String?,
+ @SerializedName("summary") val summary: String?,
+ @SerializedName("url") val url: String?,
+ @SerializedName("homepageUrl") val homepageUrl: String?,
+ @SerializedName("sourceUrl") val sourceUrl: String?,
+ @SerializedName("hide") val hide: Boolean? = false,
+ @SerializedName("readmeHTML") val readmeHTML: String?,
+ @SerializedName("scope") val scope: List