diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..03550f1 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +stage \ No newline at end of file diff --git a/.idea/aws.xml b/.idea/aws.xml new file mode 100644 index 0000000..b63b642 --- /dev/null +++ b/.idea/aws.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..dc456b8 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..ae388c2 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..103e00c --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,32 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..2b8a50f --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9f5e500 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9ec109 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Xiu +##### A browser base on Stage with Geckoview diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..b57875f --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,214 @@ + +// Optionally, set any parameters to send to the plugin. + + + +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'kotlin-android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' +} +// Optionally, set any parameters to send to the plugin. + +def versionCodeNew = 60 +def versionNameNew = "2.0" +def appName = "Xiu" + +kapt { + generateStubs = true +} + +android { + namespace 'org.mozilla.xiu.browser' + compileSdk 34 + compileSdkVersion 34 + buildFeatures { + viewBinding = true + compose true + } + defaultConfig { + minSdk 24 + targetSdk 34 + applicationId "org.mozilla.xiu.browser" + versionCode versionCodeNew + versionName versionNameNew + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + dataBinding { + enabled = true + } + composeOptions { + kotlinCompilerExtensionVersion '1.4.0' + } + + + splits { + abi { + enable true + reset() + include 'arm64-v8a', 'armeabi-v7a' + } + } + + applicationVariants.all { variant -> + variant.outputs.all { + def abi = baseName.replace("normal-", "").replace("professional-", "").replace("dev-", "").replace("-release", "").replace("-debug", "") + def pak = "" + if (abi == "universal") { + pak = "-全包" + } else if (abi == "armeabi-v7a") { + pak = "-32位" + } else if (abi == "arm64-v8a") { + pak = "-64位" + } + def fileName = appName + "_V" + versionNameNew + "_C" + versionCodeNew + pak + ".apk" + outputFileName = fileName + } + } +} + +// Optionally, set any parameters to send to the plugin. + + + + + +dependencies { + implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + + implementation 'com.google.mlkit:barcode-scanning:17.2.0' + implementation 'androidx.camera:camera-camera2:1.1.0' + implementation 'androidx.camera:camera-lifecycle:1.1.0' + implementation 'androidx.camera:camera-view:1.1.0-beta02' + + + def rxhttp_version = '3.0.1' + def lottieVersion = '6.1.0' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation "com.github.liujingxing.rxhttp:rxhttp:$rxhttp_version" + kapt "com.github.liujingxing.rxhttp:rxhttp-compiler:$rxhttp_version" + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'com.google.dagger:hilt-android:2.31.2-alpha' + annotationProcessor 'com.google.dagger:hilt-android-compiler:2.31.2-alpha' + + implementation 'androidx.activity:activity-compose:1.8.0-alpha06' + implementation platform('androidx.compose:compose-bom:2022.10.00') + implementation "androidx.activity:activity-ktx:1.2.4" + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.material:material:1.2.0-alpha06' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.8.0-alpha02' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1' + implementation 'androidx.navigation:navigation-ui-ktx:2.4.1' + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.databinding:databinding-runtime:7.1.2' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' + + + def gecko_version = "121.0.20231109165012" + def mozilla_version = "120.0b8" + implementation ("org.mozilla.geckoview:geckoview-nightly:$gecko_version") + implementation ("org.mozilla.components:browser-storage-sync:$mozilla_version") + implementation ("org.mozilla.components:service-firefox-accounts:$mozilla_version") + implementation ("org.mozilla.components:service-sync-logins:$mozilla_version") + implementation ("org.mozilla.components:support-rusthttp:$mozilla_version") + implementation ("org.mozilla.components:support-rusthttp:$mozilla_version") + implementation ("org.mozilla.components:support-rustlog:$mozilla_version") + implementation ("org.mozilla.components:lib-fetch-httpurlconnection:$mozilla_version") + implementation ("org.mozilla.components:concept-toolbar:$mozilla_version") + implementation ("org.mozilla.components:concept-storage:$mozilla_version") + implementation ("org.mozilla.components:feature-accounts-push:$mozilla_version") + + + + implementation "com.airbnb.android:lottie-compose:$lottieVersion" + implementation 'com.github.bumptech.glide:glide:4.14.2' + implementation 'com.github.bumptech.glide:annotations:4.14.2' + kapt 'com.github.bumptech.glide:compiler:4.14.2' + + def dialogx_version = "0.0.46" + implementation "com.github.kongzue.DialogX:DialogX:$dialogx_version" + implementation "com.github.kongzue.DialogX:DialogXMaterialYou:$dialogx_version" + implementation 'com.iqiyi.xcrash:xcrash-android-lib:3.1.0' + implementation 'com.squareup.moshi:moshi-kotlin:1.14.0' + implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1" + implementation "com.google.accompanist:accompanist-systemuicontroller:0.31.0-alpha" + implementation "com.github.bumptech.glide:compose:1.0.0-alpha.1" + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha05' + implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03' + + def room_version = "2.5.0" + + implementation "androidx.room:room-runtime:$room_version" + annotationProcessor "androidx.room:room-compiler:$room_version" + + // To use Kotlin annotation processing tool (kapt) + kapt "androidx.room:room-compiler:$room_version" + // To use Kotlin Symbol Processing (KSP) + + // optional - RxJava2 support for Room + implementation "androidx.room:room-rxjava2:$room_version" + + // optional - RxJava3 support for Room + implementation "androidx.room:room-rxjava3:$room_version" + + // optional - Guava support for Room, including Optional and ListenableFuture + implementation "androidx.room:room-guava:$room_version" + + // optional - Test helpers + testImplementation "androidx.room:room-testing:$room_version" + + // optional - Paging 3 Integration + implementation "androidx.room:room-paging:$room_version" + + + + implementation 'com.airbnb.android:lottie:6.0.1' + implementation "org.greenrobot:eventbus:3.0.0" + //弹窗组件 + implementation 'com.github.li-xiaojun:XPopup:2.8.0' + implementation 'com.alibaba:fastjson:1.1.70.android' + //stream用法 + implementation 'com.annimon:stream:1.2.1' + implementation 'com.jakewharton.timber:timber:4.7.1' + //白色沉浸式状态栏 + implementation 'com.githang:status-bar-compat:0.7' + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.10' + implementation group: 'org.apache.commons', name: 'commons-text', version: '1.9' +} + diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..569f575 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,106 @@ +-dontobfuscate + +#################################################################################################### +# Sentry +#################################################################################################### + +# Recommended config via https://docs.sentry.io/clients/java/modules/android/#manual-integration +# Since we don't obfuscate, we don't need to use their Gradle plugin to upload ProGuard mappings. +-keepattributes LineNumberTable,SourceFile +-dontwarn org.slf4j.** +-dontwarn javax.** + +# Our addition: this class is saved to disk via Serializable, which ProGuard doesn't like. +# If we exclude this, upload silently fails (Sentry swallows a NPE so we don't crash). +# I filed https://github.com/getsentry/sentry-java/issues/572 +# +# If Sentry ever mysteriously stops working after we upgrade it, this could be why. + +#################################################################################################### +# Android and GeckoView built-ins +#################################################################################################### + +-dontwarn android.** +-dontwarn androidx.** +-dontwarn com.google.** +-dontwarn org.mozilla.geckoview.** + +# Raptor now writes a *-config.yaml file to specify Gecko runtime settings (e.g. the profile dir). This +# file gets deserialized into a DebugConfig object, which is why we need to keep this class +# and its members. +-keep class org.mozilla.gecko.util.DebugConfig { *; } + +#################################################################################################### +# kotlinx.coroutines: use the fast service loader to init MainDispatcherLoader by including a rule +# to rewrite this property to return true: +# https://github.com/Kotlin/kotlinx.coroutines/blob/8c98180f177bbe4b26f1ed9685a9280fea648b9c/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt#L19 +# +# R8 is expected to optimize the default implementation to avoid a performance issue but a bug in R8 +# as bundled with AGP v7.0.0 causes this optimization to fail so we use the fast service loader instead. See: +# https://github.com/mozilla-mobile/focus-android/issues/5102#issuecomment-897854121 +# +# The fast service loader appears to be as performant as the R8 optimization so it's not worth the +# churn to later remove this workaround. If needed, the upstream fix is being handled in +# https://issuetracker.google.com/issues/196302685 +#################################################################################################### + + +#################################################################################################### +# Remove debug logs from release builds +#################################################################################################### +-assumenosideeffects class android.util.Log { + public static boolean isLoggable(java.lang.String, int); + public static int v(...); + public static int d(...); +} + +#################################################################################################### +# Mozilla Application Services +#################################################################################################### + +-keep class mozilla.appservices.** { *; } + +#################################################################################################### +# ViewModels +#################################################################################################### + +-keep class org.mozilla.fenix.**ViewModel { *; } + +#################################################################################################### +# Adjust +#################################################################################################### + +-keep public class com.adjust.sdk.** { *; } + +-keep public class com.android.installreferrer.** { *; } + +-keep class android.os.Build { + java.lang.String[] SUPPORTED_ABIS; + java.lang.String CPU_ABI; +} + +-keep class android.os.LocaleList { + java.util.Locale get(int); +} + +# Keep code generated from Glean Metrics +-keep class org.mozilla.fenix.GleanMetrics.** { *; } + +# Keep motionlayout internal methods +# https://github.com/mozilla-mobile/fenix/issues/2094 +-keep class androidx.constraintlayout.** { *; } + +# Keep adjust relevant classes +-keep class com.adjust.sdk.** { *; } + +-keep public class com.android.installreferrer.** { *; } + +# Keep Android Lifecycle methods +# https://bugzilla.mozilla.org/show_bug.cgi?id=1596302 +-keep class androidx.lifecycle.** { *; } + + + -keep class cn.jiguang.** { *; } + -keep class android.support.** { *; } + -keep class androidx.** { *; } + -keep class com.google.android.** { *; } \ No newline at end of file diff --git "a/app/release/Xiu_V2.0_C60-32\344\275\215.apk" "b/app/release/Xiu_V2.0_C60-32\344\275\215.apk" new file mode 100644 index 0000000..5f12a8d Binary files /dev/null and "b/app/release/Xiu_V2.0_C60-32\344\275\215.apk" differ diff --git "a/app/release/Xiu_V2.0_C60-64\344\275\215.apk" "b/app/release/Xiu_V2.0_C60-64\344\275\215.apk" new file mode 100644 index 0000000..f275ff4 Binary files /dev/null and "b/app/release/Xiu_V2.0_C60-64\344\275\215.apk" differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..7ed59c1 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,38 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "org.mozilla.xiu.browser", + "variantName": "release", + "elements": [ + { + "type": "ONE_OF_MANY", + "filters": [ + { + "filterType": "ABI", + "value": "armeabi-v7a" + } + ], + "attributes": [], + "versionCode": 60, + "versionName": "2.0", + "outputFile": "Xiu_V2.0_C60-32位.apk" + }, + { + "type": "ONE_OF_MANY", + "filters": [ + { + "filterType": "ABI", + "value": "arm64-v8a" + } + ], + "attributes": [], + "versionCode": 60, + "versionName": "2.0", + "outputFile": "Xiu_V2.0_C60-64位.apk" + } + ], + "elementType": "File" +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/thallo/stage/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/thallo/stage/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..f3c01ab --- /dev/null +++ b/app/src/androidTest/java/com/thallo/stage/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.thallo.stage + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.mozilla.xiu.browser", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0d63648 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/privacy.txt b/app/src/main/assets/privacy.txt new file mode 100644 index 0000000..dbe7c5b --- /dev/null +++ b/app/src/main/assets/privacy.txt @@ -0,0 +1,134 @@ +以下为隐私协议内容: + +Xiu隐私协议 +请您在使用本产品之前仔细阅读用户隐私协议,这份隐私协议用来阐明可拓浏览器会用收集哪些用户信息,以及使用哪些相关的联网服务。并且告知用户,作者如何保证用户的信息安全确保信息不会泄露。 + +1.集成了哪些第三方SDK与功能组件 +(1)Firefox账号登陆同步服务 +用于接入由Mozilla提供的Firefox账号登陆同步服务。协议地址:https://www.mozilla.org/zh-CN/privacy/firefox/ + +2.读写存储权限 + +虽然高版本安卓手机对于私有目录读写已不再设置权限,但是作者需要兼容部分手机,所以在您使用下载,视频播放功能时,应用依旧会向您请求这个权限,来尽量保证正常的服务。应用仅会在私有目录以及系统目录/DOWNLOAD下创建文件。 + +3.手机定位权限 + +浏览器本身不会获取用户的任何位置信息,当用户访问需要页面位置信息的页面(如地图,天气预报等)的时候浏览器内核会返回位置信息给页面服务提供商。 + +4.Xiu浏览器访问哪些网络服务? +Xiu浏览器启动的时候会请求网络,并使用一些相关的网络服务,在这里说明都使用具体哪些服务以及用途。 + +5.Xiu浏览器的网络服务程序: +包括用户数据同步,用户登录,默认程序设置,检查更新等。 +百度搜索服务程序: +用来提供浏览器的一些搜索建议。 +第三方的一些拦截规则比如ABP的拦截规则: +在应用第一次启动的时候,Xiu浏览器尝试从第三方网站下载一些拦截规则,并自动导入Xiu浏览器以便提升广告拦截效果. + +6.Xiu浏览器的麦克风使用 +当您使用语音助手功能,或者部分网页使用了webrtc技术时,应用将会向您申请此权限来提供对应功能。 + +7.Xiu浏览器的摄像头使用 +当您使用二维码扫描功能,或者部分网页使用了webrtc技术时,应用将会向您申请此权限来提供对应功能。 + +8.Xiu浏览器的悬浮于其他应用之上 +当您窗口化、小窗视频播放功能时,应用将会申请此权限。 + +9.获取传感器 +播放器将跟随系统传感器进行横竖屏调整,由于新的浏览器技术,将支持网页获取用户传感器数据。 + +第三方代码执行 +为了使您的使用过程更有可玩性,作者提供了脚本与拓展能力。你可以执行来源于第三方的代码。对于其中的高危行为,如跨域请求,应用都会对此进行拦截并提示您允许/拒绝。 + +其他 +用户通过浏览器访问任何其他网站,会留下您的一些Cookies相关信息,如您在其他网站的使用记录账号信息等,Xiu浏览器不会读取和收集这些信息,每个网站都有自己的用户隐私策略,这些和Xiu浏览器本身无关。 + +隐私策略调整 +如果以后该隐私协议会有任何变动,作者会在此更新隐私协议,以便您能够及时了解到最新的内容。 + +本声明自更新之日起生效, 最近的更新时间是:2022年6月1日 + +联系 +关于用户隐私协议的任何问题和疑问,你可以通过邮件deyinhe@qq.com联系我,作者会第一时间给与答复。 + +如有纠纷请联系deyinhe@qq.com + + + + + +以下为用户协议内容: + +Xiu用户协议 +概述 +请您在使用本产品之前仔细阅读下列条款。您下载、安装或使用产本品或者单击“同意此协议”表明您已经阅读本协议并充分理解、遵守本协议所有条款,包括涉及免除或者限制本作者责任的免责条款、用户权利限制条款、约定争议解决方式等,这些条款均用粗体字标注。如果您不同意本协议的全部或部分内容,请不要下载、安装和使用本产品。 + +权利声明 +知识产权 +本作者拥有“本产品”的所有权和知识产权等全部权利。本产品受中国及其他国家的知识产权法、国际知识产权公约(包括但不限于著作权法、商标法、专利法等)的保护。所有未授予您的权利均被本作者保留,您不可以从本产品上移除本作者的版权标记或其他权利声明。 + +软件所有权保留 +您确定不享有本软件的所有权,本软件未被出售给用户,本作者保留本软件的所有权。 + +授权许可 +本作者授予您一项非排他的、不可转让的、非商业性的、可撤销的许可,以下载、安装、备份和使用本产品。本作者授予您仅出于个人非商业目在移动设备上使用本产品,如果您希望将本产品用于其他非本作者授权的目的或其他商业目的,您必须另行取得本作者的单独书面许可。除非就某些第三方软件软件有明文规定或适用法律允许,否则您不得在未取得本作者书面许可的情况下修改、翻译、反向汇编、反向工程、反编译本软件的部分或全部。 + +用户行为 +①如果您在使用本作者产品或服务的过程中发布相关用户内容,您需要对自己发布的所有用户内容负责。用户内容是指您发布或以其他方式使用本产品时产生的所有内容(例如:您的信息、声音、图片或其他内容)。您是您的用户内容唯一的责任人,您将承担因您的用户内容违法法律、侵犯第三方权益的所有法律责任。 + +②在使用过程中,您将承担因下述行为而产生的全部法律责任,本作者不对您的下述行为承担任何责任: + +破坏宪法所确定的基本原则的; 危害国家安全、泄露国家秘密、颠覆国家政权、破坏国家统一的; 损害国家荣誉和利益的;煽动民族仇恨、民族歧视,破坏民族团结的; 破坏国家宗教政策,宣扬邪教和封建迷信的; 散布谣言,扰乱社会秩序,破坏社会稳定的;散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的; 侮辱或者诽谤他人,侵害他人合法权益的; 含有法律、行政法规禁止的其他内容的。 + +③您同意不通过本产品从事下列行为: + +发布或分享电脑病毒、蠕虫、恶意代码、故意破坏或改变计算机系统或数据的软件; + 未经授权,收集其他用户的信息或数据,例如非法收集第三人的个人信息侵犯第三人隐私或其他合法民事权益; + 用自动化的方式恶意使用本产品,给服务器造成过度的负担或以其他方式干扰或损害本产品服务器和网络链接; + 在未授权的情况下,尝试访问本产品的服务器数据或通信数据; 干扰、破坏本产品其他用户的使用; + 未经本作者授权,修改、破解、反编译、反汇编、逆向工程本产品,发布本产品的修改版、破解版等; + +免责声明 +可执行代码及插件 +本产品支持兼容一些第三方的脚本、拓展及广告拦截规则,拓展、脚本和规则来源为第三方或用户分享,您应该慎重使用,并自行承担第三方来源脚本、拓展或规则带来的后果,和本作者无关。 +本产品的一切网络资源均来自互联网,包括用户脚本,拓展,广告拦截规则等,鉴于精力有限无法做到完全的审核,请您一旦发现有任何侵权以及危害用户安全的内容,请您务必联系我们我们将第一时间将其移除。 请发送邮件至: deyinhe@qq.com + +智能播放器:网页作为互联网早期展示形式,具有极大的开放性,按照此国际惯例,对于非加密的内容,列如图片、文字、音频、视频,我方有理由认为它是开放的、无版权的、可下载、可分享的。基于此我方定制了网页播放器,给大众带来更加全面的播放体验。如果您是网页版权方,认为您的版权造成了侵害,请联系我方添加白名单。但是我方建议您为您的版权内容增加加密,成本低廉,技术难度小,否则,您的版权内容依旧是公开的,在电脑端Chrome等浏览器上,可通过审查元素轻易获取。 + +功能的调整、改进与升级 +我们可能对产品进行不时地调整、改进和增减,甚至下线我们部分产品,以不断适应我们的运营需要。任何本产品的更新版本或未来版本或者其他变更同样受到本协议约束。 + +无担保声明 +本作者在发布本产品之前,已尽可能对产品进行了详尽的技术测试和功能测试,但鉴于电子设备、操作系统、网络环境的复杂性,本作者不能保证本产品会兼容所有用户的电子设备,也无法保证用户在使用本产品过程中能够持续不出现任何技术故障。 +在法律允许的最大限度内,本作者无法对产品或服务做任何明示、暗示和强制的担保,包括但不限于软件的兼容性;产品一定满足您的需求或期望;或产品将不间断的、及时的、安全的、或无错误的运行。 +由于网络环境的自由与开放特征,我们的产品或服可能会被第三方擅自修改、破解发布于互联网,建议用户从本作者的官方应用渠道,如官网、本作者已申请认证的第三方应用商店下载、安装我们的产品,我们不会对任何非官方版本承担任何责任。 +赔偿 +赔偿。在你违反本协议或你所提供的信息侵犯第三方合法权益而导致直接或间接损失的情况下,你应当赔偿其他用户、本作者、第三方合作伙伴的所有损失、费用或支出。 +赔偿程序。可以通知你及时要求赔偿。然而,本作者未能通知不会减轻你的赔偿义务,除了在某种程度上,未能及时通知你给你造成了实质上的损害。 +额外的责任。你的赔偿义务不是本作者的唯一补救措施,除此之外可能本作者对你依据本协议采取其他补救措施,你的赔偿义务在本协议终止后仍存在。 +不可抗力与责任限制 +不可抗力:本协议有效期间,如若遭受不可抗力事件,任何一方可暂行中止履行本协议项下的义务直至不可抗力事件的影响消除,并且遭受方无需为此承担违约责任,但应及时将不可抗力事件及时通知对方,并尽最大努力克服该事件,减少损失的扩大。不可抗力指各方不能控制、不可预见或即使预见亦无法避免的事件且该事件足以妨碍、影响或延误任何一方根据本协议履行其全部或部分义务。该事件包括但不限于自然灾害、战争、法律法规变更、政府命令、计算机病毒、黑客攻击或基础电信运营商服务中断等。 +损害限制:本作者及其分支,和所属的管理人员、董事、合伙人、雇员、承包商给你造成的所有损害赔偿额度均仅限于你使用产品支付的款额。你放弃对特殊、间接、附带或间接损害要求赔偿的权利,包括并不限于利润损失、收入、使用、或数据和应用的损失,即使本作者知道此类损失的可能性。 +个人信息保护 +保护用户跟个人信息安全、维护用户隐私是我们一贯的理念,并且我们贯穿于产品的开发和运营过程。为不断优化用户体验,我们会收集一下用户使用数据,但不涉及到用户敏感的私人信息,详情请参看我们的《隐私政策》,了解关于我们收集信息的内容、使用目的以及如何保护你的信息安全,《隐私政策》均构成本协议的一部分。 + +通知 +所有有关本协议以及隐私政策的疑问、通知、要求或请求,应当用中文写作并发送至本作者邮箱:deyinhe@qq.com + +本协议的修改 +由于业务的拓展、调整或法规变化等原因,本作者可能会适时修改本协议至被法律所允许的程度。如果调整会对您的权利与义务造成重大影响,我们会尽可能通过电子邮件、应用内通知等方式告知您。我们建议您定期访查看我们的网站和移动应用程序,关注本协议的任何变化。在本协议修改后您继续使用本产品代表您接受修改后的协议内容。 + +投诉处理 +我们尊重他人合法权益,若您认为您的合法权益被侵犯,请您向我们提供书面通知。 + +书面通知务必包括:(1)权利人的姓名(名称)、联系方式和地址;(2)权利人享有权利的权属证明材料,(3)涉嫌侵权的具体情况;(4)涉嫌侵权内容在信息网络上的具体位置: (5) 请在权利通知中加入如下声明:“本人保证,本通知中所述信息真实、准确、完整,本人将承担一切法律责任。”(6) 请您签署该文件,如果您是依法成立的机构或组织,请您加盖公章。 + +请您把以上材料扫描件发送至邮箱: deyinhe@qq.com +其他 + +本协议的订立、执行和解释及争议的解决均应适用中华人民共和国法律 + +若您与我方浏览器之间发生任何争议,应友好协商解决;协商不成的,任何一方均可向合同签订地有管辖权的人民法院提起诉讼。 + +执行本协议和所有程序引起的纠纷适用法律为中华人民共和国法律、解释。由本协议引起的所有纠纷由合同签订地法院管辖。 + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..97a32b4 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/org/mozilla/xiu/browser/AboutFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/AboutFragment.kt new file mode 100644 index 0000000..5ce3a10 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/AboutFragment.kt @@ -0,0 +1,130 @@ +package org.mozilla.xiu.browser + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import com.google.android.material.bottomsheet.BottomSheetBehavior +import org.mozilla.xiu.browser.databinding.FragmentAboutBinding +import org.mozilla.xiu.browser.session.createSession +import org.mozilla.xiu.browser.utils.CommonUtil + + +class AboutFragment : Fragment() { + lateinit var fragmentAboutBinding: FragmentAboutBinding + private lateinit var SheetBehavior: BottomSheetBehavior + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + fragmentAboutBinding = FragmentAboutBinding.inflate(LayoutInflater.from(requireContext())) + + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + if (fragmentAboutBinding.aboutDrawer != null) { + SheetBehavior = + BottomSheetBehavior.from(fragmentAboutBinding.aboutDrawer as ConstraintLayout) + SheetBehavior.peekHeight = 0 + } + fragmentAboutBinding.materialButton2.setOnClickListener { sendEmail() } + fragmentAboutBinding.materialButton1.setOnClickListener { + SheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + fragmentAboutBinding.textView2.setOnClickListener { + Toast.makeText( + requireContext(), + getAppVersionName(requireContext()), + Toast.LENGTH_SHORT + ).show() + } + fragmentAboutBinding.textView11.setOnClickListener { + Toast.makeText( + requireContext(), + CommonUtil.getVersionName(context), + Toast.LENGTH_SHORT + ).show() + } + fragmentAboutBinding.materialButton5.setOnClickListener { + val intent = Intent(requireContext(), MainActivity::class.java) + + if ((getResources().getConfiguration().screenLayout and + Configuration.SCREENLAYOUT_SIZE_MASK) === + Configuration.SCREENLAYOUT_SIZE_LARGE + ) { + createSession("https://t.me/haikuoshijie6", requireActivity()) + } else { + intent.data = Uri.parse("https://t.me/haikuoshijie6") + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + } + + } + + // Inflate the layout for this fragment + return fragmentAboutBinding.root + } + + /**************** + * + * 发起添加群流程。群号:Stage浏览器chat&play(612932857) 的 key 为: TbCzUUsxKdWQqmHqgqaTFJ110tq4FqCD + * 调用 joinQQGroup(TbCzUUsxKdWQqmHqgqaTFJ110tq4FqCD) 即可发起手Q客户端申请加群 Stage浏览器chat&play(612932857) + * + * @param key 由官网生成的key + * @return 返回true表示呼起手Q成功,返回false表示呼起失败 + */ + fun joinQQGroup(key: String): Boolean { + val intent = Intent() + intent.data = + Uri.parse("mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26jump_from%3Dwebapi%26k%3D$key") + // 此Flag可根据具体产品需要自定义,如设置,则在加群界面按返回,返回手Q主界面,不设置,按返回会返回到呼起产品界面 //intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return try { + startActivity(intent) + true + } catch (e: Exception) { + // 未安装手Q或安装的版本不支持 + false + } + } + + fun getAppVersionName(context: Context): String? { + var versionName = "" + try { + val pm: PackageManager = context.getPackageManager() + val pi: PackageInfo = pm.getPackageInfo(context.getPackageName(), 0) + versionName = pi.versionName + if (versionName == null || versionName.length <= 0) { + return "" + } + } catch (e: java.lang.Exception) { + Log.e("VersionInfo", "Exception", e) + } + return versionName + } + + private fun sendEmail() { + val i = Intent(Intent.ACTION_SEND) + i.type = "message/rfc822" + i.putExtra(Intent.EXTRA_EMAIL, arrayOf("deyinhe@qq.com")) + startActivity( + Intent.createChooser( + i, + "Select email application." + ) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/AddonsManagerFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/AddonsManagerFragment.kt new file mode 100644 index 0000000..ce844a1 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/AddonsManagerFragment.kt @@ -0,0 +1,198 @@ +package org.mozilla.xiu.browser + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.WebExtension +import org.mozilla.geckoview.WebExtensionController +import org.mozilla.xiu.browser.componets.AddonsAdapter +import org.mozilla.xiu.browser.databinding.FragmentAddonsManagerBinding +import org.mozilla.xiu.browser.utils.ThreadTool +import org.mozilla.xiu.browser.utils.ToastMgr +import org.mozilla.xiu.browser.utils.UriUtilsPro +import org.mozilla.xiu.browser.webextension.WebExtensionsRefreshEvent +import org.mozilla.xiu.browser.webextension.WebextensionSession +import java.io.File + +class AddonsManagerFragment : Fragment() { + lateinit var binding: FragmentAddonsManagerBinding + private lateinit var SheetBehavior: BottomSheetBehavior + private lateinit var SheetBehavior2: BottomSheetBehavior + lateinit var webExtensionController: WebExtensionController + var adapter = AddonsAdapter() + private lateinit var launcher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (!EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().register(this) + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onWebExtensionsRefresh(event: WebExtensionsRefreshEvent) { + webExtensionController.list().accept { + adapter.submitList(it) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentAddonsManagerBinding.inflate(LayoutInflater.from(requireContext())) + webExtensionController = GeckoRuntime.getDefault(requireContext()).webExtensionController + binding.AddonsManagerRecycler.layoutManager = + LinearLayoutManager(context, RecyclerView.VERTICAL, false); + binding.AddonsManagerRecycler.adapter = adapter + webExtensionController.list().accept { + adapter.submitList(it) + } + if (binding.managerDrawer != null) { + SheetBehavior = BottomSheetBehavior.from(binding.managerDrawer as ConstraintLayout) + SheetBehavior.peekHeight = 0 + } + if (binding.addDrawer != null) { + SheetBehavior2 = BottomSheetBehavior.from(binding.addDrawer as ConstraintLayout) + SheetBehavior2.peekHeight = 0 + } + adapter.select = object : AddonsAdapter.Select { + override fun onSelect(bean: WebExtension) { + openSheet(bean) + } + } + binding.materialButton4.setOnClickListener { + openAddSheet() + } + launcher = registerForActivityResult( + object : ActivityResultContract() { + override fun createIntent(context: Context, input: Boolean): Intent { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "*/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): Intent { + return intent!! + } + + }, + object : + ActivityResultCallback { + override fun onActivityResult(result: Intent?) { + if (result == null || result.data == null) { + return + } + val uri = result.data + val path: String = + UriUtilsPro.getRootDir(context) + File.separator + "_cache" + File.separator + UriUtilsPro.getFileName( + uri + ) + UriUtilsPro.getFilePathFromURI( + context, + uri, + path, + object : UriUtilsPro.LoadListener { + override fun success(s: String) { + ThreadTool.runOnUI { + val p = "file://$s" + WebextensionSession(requireActivity()).install(p) + } + } + + override fun failed(msg: String) { + ToastMgr.shortBottomCenter( + context, + "出错:$msg" + ) + } + } + ) + } + }) + return binding.root + } + + fun openSheet(extension: WebExtension) { + var metadata: WebExtension.MetaData = extension.metaData + metadata.icon.getBitmap(72).accept { binding.imageView7.setImageBitmap(it) } + binding.textView12.text = metadata.name + binding.textView18.text = metadata.creatorName + binding.textView19.text = metadata.version + binding.textView20.text = metadata.description + SheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED + if (metadata.optionsPageUrl != null) + binding.button3.visibility = View.VISIBLE + else + binding.button3.visibility = View.GONE + + binding.button3.setOnClickListener { + val intent = Intent(requireContext(), MainActivity::class.java) + intent.data = Uri.parse(metadata.optionsPageUrl) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + + } + binding.button2.setOnClickListener { + webExtensionController.uninstall(extension).accept { + webExtensionController.list().accept { + adapter.submitList(it) + } + } + SheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + + } + } + + private fun openAddSheet() { + SheetBehavior2.state = BottomSheetBehavior.STATE_HALF_EXPANDED + binding.addFromFirefox.setOnClickListener { + val intent = Intent(requireContext(), MainActivity::class.java) + intent.data = Uri.parse("https://addons.mozilla.org/zh-CN/firefox/") + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + } + binding.addFromLocal.setOnClickListener { + SheetBehavior2.state = BottomSheetBehavior.STATE_COLLAPSED + launcher.launch(true) + } + binding.addFromOther.setOnClickListener { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("使用协议") + .setMessage("第三方网站内的扩展程序由网站所有者提供,网站内所有扩展、广告、内容等均与本软件无关,请注意甄别信息真假性、合法性、合规性,由此产生的任何争议,应与网站所有者协商或诉讼解决") + .setPositiveButton("同意") { _, _ -> + val intent = Intent(requireContext(), MainActivity::class.java) + intent.data = Uri.parse("https://www.crxsoso.com/") + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + }.setNegativeButton("取消") { d, _ -> + d.dismiss() + }.show() + } + } + + override fun onDestroy() { + super.onDestroy() + if (EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().unregister(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/App.kt b/app/src/main/java/org/mozilla/xiu/browser/App.kt new file mode 100644 index 0000000..eeae51a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/App.kt @@ -0,0 +1,42 @@ +package org.mozilla.xiu.browser + +import android.app.Application +import android.content.Context +import android.widget.Toast +import com.google.android.material.color.DynamicColors +import com.kongzue.dialogx.DialogX +import java.util.Timer +import kotlin.concurrent.timerTask + +open class App: Application() { + + override fun onCreate() { + super.onCreate() + // apply dynamic color + DynamicColors.applyToActivitiesIfAvailable(this) + DialogX.init(this) + //syncLooper() + application = this + } + + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + xcrash.XCrash.init(this) + + + } + + + fun syncLooper(){ + val timer=Timer() + timer.schedule(timerTask { + Toast.makeText(applicationContext,"",Toast.LENGTH_SHORT) + + },5000,5000) + } + + companion object { + @JvmField + var application: Application? = null + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/BottomBarFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/BottomBarFragment.kt new file mode 100644 index 0000000..b5703db --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/BottomBarFragment.kt @@ -0,0 +1,13 @@ +package org.mozilla.xiu.browser + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + diff --git a/app/src/main/java/org/mozilla/xiu/browser/HolderActivity.kt b/app/src/main/java/org/mozilla/xiu/browser/HolderActivity.kt new file mode 100644 index 0000000..c86b87b --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/HolderActivity.kt @@ -0,0 +1,78 @@ +package org.mozilla.xiu.browser + +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.mozilla.xiu.browser.databinding.ActivityHolderBinding +import org.mozilla.xiu.browser.utils.StatusUtils +import org.mozilla.xiu.browser.webextension.BrowseEvent +import org.mozilla.xiu.browser.webextension.WebExtensionsAddEvent + +class HolderActivity : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityHolderBinding + + override fun onCreate(savedInstanceState: Bundle?) { + WindowCompat.setDecorFitsSystemWindows(window, false) + super.onCreate(savedInstanceState) + + if (!EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().register(this) + } + + binding = ActivityHolderBinding.inflate(layoutInflater) + setContentView(binding.root) + StatusUtils.init(this) + + setSupportActionBar(binding.toolbar) + + val navController = findNavController(R.id.nav_host_fragment_content_holder) + val navInflater = navController.navInflater + val navGraph = navInflater.inflate(R.navigation.nav_graph2) + when (intent.getStringExtra("Page")) { + "DOWNLOAD" -> navGraph.setStartDestination(R.id.downloadFragment) + "ADDONS" -> navGraph.setStartDestination(R.id.addonsManagerFragment) + "SETTINGS" -> navGraph.setStartDestination(R.id.settingsFragment) + "QRSCANNING" -> { + navGraph.setStartDestination(R.id.qrScanningFragment) + binding.toolbar.visibility = View.GONE + } + + } + navController.graph=navGraph + appBarConfiguration = AppBarConfiguration(navController.graph) + setupActionBarWithNavController(navController, appBarConfiguration) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun addWebExtension(event: WebExtensionsAddEvent) { + finish() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun browse(event: BrowseEvent) { + finish() + } + + override fun onDestroy() { + super.onDestroy() + if (EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().unregister(this) + } + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_content_holder) + return navController.navigateUp(appBarConfiguration) + || super.onSupportNavigateUp() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/HomeFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/HomeFragment.kt new file mode 100644 index 0000000..9fd4f4d --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/HomeFragment.kt @@ -0,0 +1,244 @@ +package org.mozilla.xiu.browser + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import android.util.Patterns +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.URLUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentTransaction +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import com.bumptech.glide.Glide +import org.mozilla.xiu.browser.broswer.bookmark.shortcut.ShortcutAdapter +import org.mozilla.xiu.browser.componets.StageFxAEntryPoint +import org.mozilla.xiu.browser.componets.TabBottomSheetDialog.Companion.TAG +import org.mozilla.xiu.browser.componets.popup.AccountPopup +import org.mozilla.xiu.browser.database.shortcut.Shortcut +import org.mozilla.xiu.browser.database.shortcut.ShortcutViewModel +import org.mozilla.xiu.browser.databinding.FragmentFirstBinding +import org.mozilla.xiu.browser.fxa.AccountManagerCollection +import org.mozilla.xiu.browser.fxa.Fxa +import org.mozilla.xiu.browser.fxa.AccountProfileViewModel +import org.mozilla.xiu.browser.session.* +import kotlinx.coroutines.launch +import mozilla.components.service.fxa.FxaAuthData +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.toAuthType +import mozilla.components.support.rustlog.RustLog +import java.util.* + +/** + * A simple [Fragment] subclass as the default destination in the navigation. + */ +class HomeFragment : Fragment() { + + private var _binding: FragmentFirstBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + lateinit var geckoViewModel: GeckoViewModel + private lateinit var fxaViewModel :AccountProfileViewModel + private lateinit var accountManagerCollection :AccountManagerCollection + private var fxaAccountManager: FxaAccountManager? = null + private lateinit var fxa: Fxa + lateinit var shortcutViewModel: ShortcutViewModel + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + _binding = FragmentFirstBinding.inflate(inflater, container, false) + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + geckoViewModel = ViewModelProvider(requireActivity())[GeckoViewModel::class.java] + shortcutViewModel=ViewModelProvider(requireActivity())[ShortcutViewModel::class.java] + fxaViewModel = ViewModelProvider(requireActivity())[AccountProfileViewModel::class.java] + accountManagerCollection = ViewModelProvider(requireActivity())[AccountManagerCollection::class.java] + try { + fxa = Fxa() + try { + RustLog.disable() + } catch (e: Exception) { + e.printStackTrace() + } + fxaAccountManager = fxa.init(requireContext()) + } catch (e: Exception) { + e.printStackTrace() + } + + + lifecycleScope.launch { + + fxaViewModel.data.collect(){ + binding.signinButton?.let { it1 -> + Glide.with(requireContext()).load(it.avatar).circleCrop().into( + it1 + ) + } + } + } + + lifecycleScope.launch { + accountManagerCollection.data.collect(){ + fxaAccountManager = it + Log.d("fxaAccountManager",""+it) + + } + } + + + + + + + + binding.signinButton?.setOnClickListener { + lifecycleScope.launch { + if (!fxa.isLogin){ + fxaAccountManager?.beginAuthentication(entrypoint =StageFxAEntryPoint.DeepLink)?.let { + createSession(it,requireActivity()) + } + } + else{ + //fxaAccountManager.syncNow(SyncReason.User) + //fxaAccountManager.authenticatedAccount()?.deviceConstellation()?.pollForCommands() + AccountPopup().show(parentFragmentManager,TAG) + + } + } + } + binding.qrButton?.setOnClickListener { + if (requireActivity().checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + var intent= Intent(requireContext(), HolderActivity::class.java) + intent.putExtra("Page","QRSCANNING") + requireContext().startActivity(intent) + } + else requireActivity().requestPermissions(arrayOf(Manifest.permission.CAMERA), 1) + + } + + + binding.HomeSearchText?.setOnKeyListener(View.OnKeyListener { view, i, keyEvent -> + if (KeyEvent.KEYCODE_ENTER == i && keyEvent.action == KeyEvent.ACTION_DOWN) { + var value= binding.HomeSearchText!!.text.toString() + if (Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) { + createSession(value, requireActivity()) + + } else { + createSession("https://cn.bing.com/search?q=$value",requireActivity()) + } + + } + false + }) + + DelegateLivedata.getInstance().observe(viewLifecycleOwner){ + it.login=object : SessionDelegate.Login{ + override fun onLogin(code: String, state: String, action: String) { + lifecycleScope.launch { + fxaAccountManager?.finishAuthentication( + FxaAuthData(action.toAuthType(), code = code, state = state), + ) + + } + + } + } + } + + val calendar = Calendar.getInstance() + + if (calendar[Calendar.HOUR_OF_DAY] in 6..11) { + binding.tips?.text = "Good\nMorning" + } + if (calendar[Calendar.HOUR_OF_DAY] in 12..13) { + binding.tips?.text = "Good\n" + "Noon" + } + if (calendar[Calendar.HOUR_OF_DAY] in 14..19) { + binding.tips?.text = "Good\n" + "Afternoon" + } + if (calendar[Calendar.HOUR_OF_DAY] in 20..22) { + binding.tips?.text = "Good\n" + "Night" + } + if (22 < calendar[Calendar.HOUR_OF_DAY]) { + binding.tips?.text = "Good\nDream" + } + if (calendar[Calendar.HOUR_OF_DAY] in 0..4) { + binding.tips?.text = "Good\nDream" + } + + + var shortcutAdapter= ShortcutAdapter() + binding.shortcutsRecyclerView?.adapter =shortcutAdapter + binding.shortcutsRecyclerView?.layoutManager = GridLayoutManager(context, 4) + shortcutViewModel.allShortcutsLive?.observe(requireActivity()){ + shortcutAdapter.submitList(it) + } + + shortcutAdapter.select= object : ShortcutAdapter.Select { + override fun onSelect(url: String) { + createSession(url,requireActivity()) + } + + } + shortcutAdapter.longClick= object : ShortcutAdapter.LongClick { + override fun onLongClick(bean: Shortcut) { + shortcutViewModel.deleteShortcuts(bean) + } + + + } + + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 + } + + override fun onDestroy() { + super.onDestroy() + try { + fxaAccountManager?.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + fun showDialog() { + val fragmentManager = parentFragmentManager + val newFragment = AccountPopup() + if (false) { + // The device is using a large layout, so show the fragment as a dialog + newFragment.show(fragmentManager, "dialog") + } else { + // The device is smaller, so show the fragment fullscreen + val transaction = fragmentManager.beginTransaction() + // For a little polish, specify a transition animation + transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + // To make it fullscreen, use the 'content' root view as the container + // for the fragment, which is always the root view for the activity + transaction + .add(android.R.id.content, newFragment) + .addToBackStack(null) + .commit() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/LabHomeFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/LabHomeFragment.kt new file mode 100644 index 0000000..9adf5fc --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/LabHomeFragment.kt @@ -0,0 +1,111 @@ +package org.mozilla.xiu.browser + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.viewinterop.AndroidView +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoView +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.core.view.WindowCompat +import com.google.accompanist.systemuicontroller.rememberSystemUiController + +class LabHomeFragment : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return ComposeView(requireContext()).apply { + + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false) + + setContent { + TransparentSystemBars() + MaterialTheme() { + ConstraintLayout { + val (button,geckoView) = createRefs() + GeckoView(modifier = Modifier.constrainAs(geckoView) { + width = Dimension.fillToConstraints + height = Dimension.fillToConstraints + top.linkTo(parent.top,0.dp) + bottom.linkTo(parent.bottom, 0.dp) + start.linkTo(parent.start,0.dp) + end.linkTo(parent.end, 0.dp) + }) + + /**Button(onClick = { /*TODO*/ }, modifier = Modifier + .width(64.dp) + .height(64.dp) + .alpha(0.5f) + .blur( + radiusX = 2.dp, + radiusY = 2.dp, + edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(0.dp)) + ) + .constrainAs(button) { + bottom.linkTo(parent.bottom, 0.dp) + end.linkTo(parent.end, 0.dp) + }, colors = ButtonDefaults.buttonColors(Color.White) + ) { + + }**/ + + } + + + } + } + } + } + @Composable + fun GeckoView(modifier: Modifier){ + AndroidView( + modifier = modifier, // Occupy the max size in the Compose UI tree + factory = { context -> + // Creates custom view + GeckoView(context).apply { + // Sets up listeners for View -> Compose communication + this.releaseSession() + val session=GeckoSession() + session.open(GeckoRuntime.getDefault(this@LabHomeFragment.requireContext())) + session.loadUri("https://inftab.com/") + this.setSession(session) + + } + }, + update = { view -> + // View's been inflated or state read in this block has been updated + // Add logic here if necessary + // As selectedItem is read here, AndroidView will recompose + // whenever the state changes + // Example of Compose -> View communication + // view.coordinator.selectedItem = selectedItem.value + + } + ) + } + @Composable + fun TransparentSystemBars() { + val systemUiController = rememberSystemUiController() + val useDarkIcons = isSystemInDarkTheme() + SideEffect { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons, + isNavigationBarContrastEnforced = false,) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/MainActivity.kt b/app/src/main/java/org/mozilla/xiu/browser/MainActivity.kt new file mode 100644 index 0000000..f273ef0 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/MainActivity.kt @@ -0,0 +1,480 @@ +package org.mozilla.xiu.browser + + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.util.AttributeSet +import android.util.Log +import android.util.Patterns +import android.view.KeyEvent +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.webkit.URLUtil +import androidx.activity.OnBackPressedCallback +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.setupWithNavController +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.sidesheet.SideSheetBehavior +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.xiu.browser.broswer.dialog.SearchDialog +import org.mozilla.xiu.browser.broswer.home.TipsAdapter +import org.mozilla.xiu.browser.componets.BookmarkDialog +import org.mozilla.xiu.browser.componets.CollectionAdapter +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.componets.popup.MenuPopup +import org.mozilla.xiu.browser.componets.popup.TabPopup +import org.mozilla.xiu.browser.database.history.HistoryViewModel +import org.mozilla.xiu.browser.databinding.ActivityMainBinding +import org.mozilla.xiu.browser.databinding.PrivacyAgreementLayoutBinding +import org.mozilla.xiu.browser.session.* +import org.mozilla.xiu.browser.tab.AddTabLiveData +import org.mozilla.xiu.browser.tab.DelegateListLiveData +import org.mozilla.xiu.browser.tab.RemoveTabLiveData +import org.mozilla.xiu.browser.tab.TabListAdapter +import org.mozilla.xiu.browser.utils.FileUtil +import org.mozilla.xiu.browser.utils.FullScreen +import org.mozilla.xiu.browser.utils.SoftKeyBoardListener.OnSoftKeyBoardChangeListener +import org.mozilla.xiu.browser.utils.StatusUtils +import org.mozilla.xiu.browser.utils.ThreadTool +import org.mozilla.xiu.browser.utils.UriUtilsPro +import org.mozilla.xiu.browser.utils.getSizeName +import org.mozilla.xiu.browser.webextension.BrowseEvent +import org.mozilla.xiu.browser.webextension.WebExtensionsAddEvent +import org.mozilla.xiu.browser.webextension.WebextensionSession +import java.io.File + + +/** + * 2023.1.4创建,1.21除夕 + * 2023.2.11 19:10 正月廿一 记录 + * thallo + **/ +class MainActivity : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMainBinding + private lateinit var privacyAgreementLayoutBinding: PrivacyAgreementLayoutBinding + var fragments = listOf(HomeFragment(), WebFragment { full -> fullScreenCall(full) }) + private lateinit var geckoViewModel: GeckoViewModel + var sessionDelegates = ArrayList() + private val adapter = TabListAdapter() + var bottomSheetBehavior: BottomSheetBehavior? = null + lateinit var sideSheetBehavior: SideSheetBehavior + var isHome: Boolean = true + lateinit var historyViewModel: HistoryViewModel + var searching = "" + + private fun fullScreenCall(fullScreen: Boolean) { + if (fullScreen) { + StatusUtils.setStatusBarVisibility(this, false, binding.containerView!!) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + StatusUtils.setStatusBarVisibility(this, true, binding.containerView!!) + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + privacyAgreementLayoutBinding = PrivacyAgreementLayoutBinding.inflate(layoutInflater) + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + + if (!EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().register(this) + } + + privacyAgreementLayoutBinding.materialButton14.setOnClickListener { + setContentView(binding.root) + prefs.edit().putBoolean("privacy1", true).commit() + } + privacyAgreementLayoutBinding.materialButton17.setOnClickListener { + setContentView(binding.root) + prefs.edit().putBoolean("privacy1", true).commit() + } + if (prefs.getBoolean("privacy1", false)) + setContentView(binding.root) + else + setContentView(privacyAgreementLayoutBinding.root) + privacyAgreementLayoutBinding.webView.loadUrl("file:///android_asset/privacy.txt") + + StatusUtils.init(this) + WebextensionSession(this) + + onBackPressedDispatcher.addCallback(this, onBackPress) + geckoViewModel = ViewModelProvider(this)[GeckoViewModel::class.java] + historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java] + if (binding.content.drawer != null) { + bottomSheetBehavior = + BottomSheetBehavior.from(binding.content.drawer as ConstraintLayout) + bottomSheetBehavior!!.peekHeight = 0 + bottomSheetBehavior!!.isDraggable = false + } + + adapter.select = object : TabListAdapter.Select { + override fun onSelect() {} + } + + binding.SearchText?.imeOptions = EditorInfo.IME_ACTION_SEARCH + + + binding.materialButton13?.setOnClickListener { + var searchDialog = SearchDialog(this) + searchDialog.setOnDismissListener { + when (org.mozilla.xiu.browser.broswer.SearchEngine(this)) { + getString(org.mozilla.xiu.browser.R.string.baidu) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Baidu)) + + getString(org.mozilla.xiu.browser.R.string.google) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Google)) + + getString(org.mozilla.xiu.browser.R.string.bing) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Bing)) + + getString(org.mozilla.xiu.browser.R.string.sogou) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Sougou)) + getString(org.mozilla.xiu.browser.R.string.sk360) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.s360)) + getString(org.mozilla.xiu.browser.R.string.wuzhui) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Wuzhui)) + getString(org.mozilla.xiu.browser.R.string.yandex) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Yandex)) + getString(org.mozilla.xiu.browser.R.string.shenma) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Shenma)) + } + if (prefs.getBoolean("switch_diy", false)) + binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.diySearching)) + } + searchDialog.show() + + } + binding.SearchText?.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + binding.bottomMotionLayout?.transitionToEnd() + binding.constraintLayout10?.visibility = View.VISIBLE + when (org.mozilla.xiu.browser.broswer.SearchEngine(this)) { + getString(org.mozilla.xiu.browser.R.string.baidu) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Baidu)) + + getString(org.mozilla.xiu.browser.R.string.google) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Google)) + + getString(org.mozilla.xiu.browser.R.string.bing) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Bing)) + + getString(org.mozilla.xiu.browser.R.string.sogou) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Sougou)) + getString(org.mozilla.xiu.browser.R.string.sk360) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.s360)) + getString(org.mozilla.xiu.browser.R.string.wuzhui) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Wuzhui)) + getString(org.mozilla.xiu.browser.R.string.yandex) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Yandex)) + getString(org.mozilla.xiu.browser.R.string.shenma) -> binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.Shenma)) + } + if (prefs.getBoolean("switch_diy", false)) + binding.materialButton13?.text = + getString(R.string.EngineTips, getString(R.string.diySearching)) + } else { + binding.bottomMotionLayout?.transitionToStart() + binding.constraintLayout10?.visibility = View.GONE + if (!isHome) + binding.SearchText?.setText(binding.user?.u) + } + } + binding.materialButtonClear?.setOnClickListener { binding.SearchText?.setText("") } + var tipsAdapter = TipsAdapter() + binding.recyclerView4?.adapter = tipsAdapter + binding.recyclerView4?.layoutManager = LinearLayoutManager(this) + tipsAdapter.select = object : TipsAdapter.Select { + override fun onSelect(url: String) { + searching(url) + val imm: InputMethodManager = + getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + // 隐藏软键盘 + imm.hideSoftInputFromWindow( + window.decorView.windowToken, + 0 + ) + + } + + } + binding.SearchText?.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + var s1 = s.toString().trim() + if (s1 != "") { + historyViewModel.findHistoriesWithMix(s1)?.observe(this@MainActivity) { + tipsAdapter.submitList(it) + } + } + + + } + + override fun afterTextChanged(s: Editable?) { + + } + }) + + binding.SearchText?.setOnKeyListener(View.OnKeyListener { _, i, keyEvent -> + if (KeyEvent.KEYCODE_ENTER == i && keyEvent.action == KeyEvent.ACTION_DOWN) { + var value = binding.SearchText!!.text.toString() + searching(value) + searching = value + } + + false + }) + binding.materialButtonEdit?.setOnClickListener { binding.SearchText?.setText(searching) } + binding.urlText?.setOnKeyListener(View.OnKeyListener { _, i, keyEvent -> + if (KeyEvent.KEYCODE_ENTER == i && keyEvent.action == KeyEvent.ACTION_DOWN) { + var value = binding.urlText!!.text.toString() + searching(value) + + } + false + }) + binding.recyclerView2?.adapter = adapter + binding.recyclerView2?.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + binding.content.viewPager.adapter = CollectionAdapter(this, fragments) + binding.content.viewPager.isUserInputEnabled = false + binding.materialButtonMenu?.setOnClickListener { MenuPopup(this).show() } + binding.materialButtonHome?.setOnClickListener { HomeLivedata.getInstance().Value(true) } + binding.materialButtonTab?.setOnClickListener { TabPopup(this).show() } + binding.addButton?.setOnClickListener { HomeLivedata.getInstance().Value(true) } + binding.content.popupCloseButton?.setOnClickListener { + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } + if (binding.content.viewPager.currentItem == 1) { + + binding.reloadButtonL?.isClickable = false + binding.forwardButtonL?.isClickable = false + binding.backButtonL?.isClickable = false + } else { + binding.backButtonL?.setOnClickListener { + if (binding.user?.canBack == true) + binding.user?.session?.goBack() + else RemoveTabLiveData.getInstance().Value(sessionDelegates.indexOf(binding.user)) + } + binding.forwardButtonL?.setOnClickListener { + if (binding.user?.canForward == true) + binding.user?.session?.goForward() + } + binding.reloadButtonL?.setOnClickListener { + binding.user?.session?.reload() + } + } + binding.menuButton?.setOnClickListener { + openMenu() + } + binding.addonsButton?.setOnClickListener { + if (!isHome) + BookmarkDialog(this, binding.user!!.mTitle, binding.user!!.u).show() + } + binding.content.viewPager.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + if (position == 0) + binding.SearchText?.setText("") + } + + }) + + + + + DelegateLivedata.getInstance().observe(this) { + binding.user = it + } + DelegateListLiveData.getInstance().observe(this) { + binding.SizeText?.setText(it.size.toString()) + adapter.submitList(it.toList()) + sessionDelegates = it + } + HomeLivedata.getInstance().observe(this) { + isHome = it + if (it) { + binding.content.viewPager.currentItem = 0 + binding.urlText?.setText("") + } else { + binding.content.viewPager.currentItem = 1 + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + AddTabLiveData.getInstance().observe(this) { + binding.recyclerView2?.smoothScrollToPosition(it) + } + + + if (getSizeName(this) == "large") + FullScreen(this) + + val uri: Uri? = intent?.data + if (uri != null) { + createSession(uri.toString(), this) + } + ThreadTool.async { + val fileDirPath: String = + UriUtilsPro.getRootDir(getContext()) + File.separator + "_cache" + if (File(fileDirPath).exists()) { + FileUtil.deleteDirs(fileDirPath) + } + } + } + + override fun onDestroy() { + super.onDestroy() + if (EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().unregister(this) + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun addWebExtension(event: WebExtensionsAddEvent) { + Log.d("open", "open addWebExtension: ") + WebextensionSession(this).install("file://${event.path}") + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun browse(event: BrowseEvent) { + searching(event.url) + } + + @Subscribe + fun onProgressUpdate(event: ProgressEvent) { + binding.progress?.setWebProgress(event.progress) + } + + private fun getContext(): Context { + return this + } + + override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { + + + return super.onCreateView(name, context, attrs) + } + + private val onBackPress = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (binding.user?.isFull == true) { + binding.user?.session?.exitFullScreen() + } else if (binding.user?.canBack == true) { + binding.user?.session?.goBack() + } else { + if (sessionDelegates.indexOf(binding.user) != -1) + RemoveTabLiveData.getInstance().Value(sessionDelegates.indexOf(binding.user)) + else { + finish() + } + } + } + } + + private fun openMenu() { + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + var navController = findNavController(R.id.fragmentContainerView3) + binding.content.navigationrail?.setupWithNavController(navController) + binding.content.appbar?.setupWithNavController( + navController, + AppBarConfiguration(navController.graph) + ) + if (isHome) + binding.content.navigationrail?.headerView?.findViewById(R.id.floatingActionButton)?.visibility = + View.GONE + else + binding.content.navigationrail?.headerView?.findViewById(R.id.floatingActionButton)?.visibility = + View.VISIBLE + + binding.content.navigationrail?.headerView?.findViewById(R.id.floatingActionButton) + ?.setOnClickListener { + navController.navigate(R.id.addonsPopupFragment2) + } + + + } + + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + val uri: Uri? = intent?.data + if (uri != null) { + createSession(uri.toString(), this) + } + GeckoRuntime.getDefault(this).activityDelegate = GeckoRuntime.ActivityDelegate { + Log.d("test", uri.toString()) + GeckoResult.fromValue(Intent()) + } + } + + override fun onStart() { + super.onStart() + + } + + override fun onResume() { + super.onResume() + //binding.user?.resume() + + org.mozilla.xiu.browser.utils.SoftKeyBoardListener.setListener( + this, + object : OnSoftKeyBoardChangeListener { + override fun keyBoardShow(height: Int) { + } + + override fun keyBoardHide(height: Int) { + binding.constraintLayout10?.visibility = View.GONE + + binding.bottomMotionLayout?.transitionToStart() + binding.SearchText?.clearFocus() + } + }) + } + + fun searching(value: String) { + if (Patterns.WEB_URL.matcher(value) + .matches() || URLUtil.isValidUrl(value) || value.startsWith("about:") + ) { + if (binding.content.viewPager.currentItem == 1) + binding.user?.session?.loadUri(value) + else + createSession(value, this) + } else { + if (binding.content.viewPager.currentItem == 1) + binding.user?.session?.loadUri("${org.mozilla.xiu.browser.broswer.SearchEngine(this)}$value") + else + createSession("${org.mozilla.xiu.browser.broswer.SearchEngine(this)}$value", this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/PrivacyAndServiceFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/PrivacyAndServiceFragment.kt new file mode 100644 index 0000000..28ae94a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/PrivacyAndServiceFragment.kt @@ -0,0 +1,28 @@ +package org.mozilla.xiu.browser + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.mozilla.xiu.browser.databinding.FragmentPrivacyAndServiceBinding + +class PrivacyAndServiceFragment : Fragment() { + lateinit var fragmentPrivacyAndServiceBinding: FragmentPrivacyAndServiceBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + fragmentPrivacyAndServiceBinding = FragmentPrivacyAndServiceBinding.inflate(LayoutInflater.from(context)) + fragmentPrivacyAndServiceBinding.webView2.loadUrl("file:///android_asset/privacy.txt") + // Inflate the layout for this fragment + return fragmentPrivacyAndServiceBinding.root + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/WebFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/WebFragment.kt new file mode 100644 index 0000000..ab56ec8 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/WebFragment.kt @@ -0,0 +1,208 @@ +package org.mozilla.xiu.browser + +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.core.graphics.drawable.toBitmap +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.WebRequestError +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.databinding.FragmentSecondBinding +import org.mozilla.xiu.browser.session.DelegateLivedata +import org.mozilla.xiu.browser.session.GeckoViewModel +import org.mozilla.xiu.browser.session.PrivacyFlow +import org.mozilla.xiu.browser.session.SessionDelegate +import org.mozilla.xiu.browser.tab.AddTabLiveData +import org.mozilla.xiu.browser.tab.DelegateListLiveData +import org.mozilla.xiu.browser.tab.RemoveTabLiveData +import org.mozilla.xiu.browser.utils.filePicker.FilePicker +import org.mozilla.xiu.browser.utils.filePicker.PickUtils.getPath + + +/** + * 2023.1.4创建,1.21除夕 + * 2023.2.11 19:10 正月廿一 记录 + * thallo + **/ +class WebFragment( + var fullscreenCall: (full: Boolean) -> Unit +) : Fragment() { + + private var _binding: FragmentSecondBinding? = null + lateinit var session: GeckoSession + lateinit var geckoViewModel: GeckoViewModel + lateinit var privacyFlow: PrivacyFlow + lateinit var delegate: ArrayList + lateinit var uri: Uri + lateinit var sessiondelegate: SessionDelegate + var mPosX: Float = 0f + var mPosY: Float = 0f + var mCurPosX: Float = 0f + var mCurPosY: Float = 0f + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + var active: Int = -1 + lateinit var filePicker: FilePicker + var isPrivacy: Boolean = false + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentSecondBinding.inflate(inflater, container, false) + geckoViewModel = ViewModelProvider(requireActivity())[GeckoViewModel::class.java] + privacyFlow = ViewModelProvider(requireActivity())[PrivacyFlow::class.java] + return binding.root + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val launcher: ActivityResultLauncher = + registerForActivityResult( + org.mozilla.xiu.browser.utils.filePicker.ResultContract(), + object : ActivityResultCallback { + override fun onActivityResult(result: Intent?) { + if (result == null) { + return + } + val uri = result.data + //文件路径 + val mFilePath = getPath(context!!, uri!!) + Log.d("ActivityResultLauncher", mFilePath) + filePicker.putUri(Uri.parse("file://$mFilePath")) + } + }) + + + filePicker = FilePicker(launcher, requireActivity()) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + delegate = ArrayList() + + lifecycleScope.launch { + privacyFlow + .data + .collect() { v -> + isPrivacy = v + Log.d("privacyFlow", "" + v) + + } + } + lifecycleScope.launch { + geckoViewModel.data.collect { value: GeckoSession -> + openSession(value) + //Toast.makeText(context,"ok",Toast.LENGTH_SHORT).show() + } + + } + + DelegateListLiveData.getInstance().observe(viewLifecycleOwner) { + delegate = it + } + RemoveTabLiveData.getInstance().observe(viewLifecycleOwner) { + if (delegate.size != 0) { + delegate[it].close() + delegate.removeAt(it) + + if (delegate.getOrNull(it) == null) { + if (delegate.getOrNull(it - 1) == null) HomeLivedata.getInstance().Value(true) + else DelegateLivedata.getInstance().Value(delegate[it - 1]) + } else + DelegateLivedata.getInstance().Value(delegate[it]) + + DelegateListLiveData.getInstance().Value(delegate) + } + } + DelegateLivedata.getInstance().observe(viewLifecycleOwner) { + for (i in delegate) { + if (it != i) + i.active = false + } + it.active = true + active = delegate.indexOf(it) + AddTabLiveData.getInstance().Value(active) + binding.geckoview.session?.setActive(false) + binding.geckoview.releaseSession() + GeckoRuntime.getDefault(requireContext()).webExtensionController.setTabActive( + it.session, + true + ) + binding.geckoview.setSession(it.session) + sessiondelegate = it + } + } + + fun openSession(session: GeckoSession) { + binding.geckoview.releaseSession() + val sessionDelegate: SessionDelegate? = + activity?.let { + SessionDelegate(it, session, filePicker, isPrivacy) { full -> + fullscreenCall(full) + } + } + if (sessionDelegate != null) { + sessionDelegate.setpic = object : SessionDelegate.Setpic { + override fun onSetPic() { + binding.geckoview.capturePixels().accept { + if (it != null) { + if (sessionDelegate.privacy) + sessionDelegate.bitmap = + requireActivity().getDrawable(R.drawable.close_outline) + ?.toBitmap()!! + else + sessionDelegate.bitmap = it + } + } + } + + } + sessionDelegate.pageError = object : SessionDelegate.PageError { + override fun onPageChange() { + binding.errorpage.visibility = View.GONE + } + + override fun onPageError( + session: GeckoSession, + uri: String?, + error: WebRequestError + ) { + binding.errorpage.visibility = View.VISIBLE + binding.errorCodeText.text = "Error:${error.code}" + binding.errorRetryButton.setOnClickListener { session.reload() } + + } + } + } + if (delegate.size == 0) + sessionDelegate?.let { delegate.add(it) } + else + sessionDelegate?.let { delegate.add(active + 1, it) } + sessionDelegate?.let { DelegateLivedata.getInstance().Value(it) } + DelegateListLiveData.getInstance().Value(delegate) + binding.geckoview.coverUntilFirstPaint(Color.WHITE) + binding.geckoview.setSession(session) + } + + override fun onResume() { + super.onResume() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/base/BaseActivity.java b/app/src/main/java/org/mozilla/xiu/browser/base/BaseActivity.java new file mode 100644 index 0000000..4f26fdd --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/base/BaseActivity.java @@ -0,0 +1,171 @@ +package org.mozilla.xiu.browser.base; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; + +import androidx.annotation.IdRes; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import org.jetbrains.annotations.NotNull; +import org.mozilla.xiu.browser.R; +import org.mozilla.xiu.browser.utils.DisplayUtil; +import org.mozilla.xiu.browser.utils.MyStatusBarUtil; +import org.mozilla.xiu.browser.utils.PreferenceConstant; +import org.mozilla.xiu.browser.utils.PreferenceMgr; + +import timber.log.Timber; + +public abstract class BaseActivity extends AppCompatActivity { + private static final String TAG = "BaseActivity"; + protected Bundle extraDataBundle; + private boolean hasInit = false; + protected boolean drawStatusBar = true; + + @Override + protected void onNewIntent(Intent intent) { + Timber.d("onNewIntent===>%s", getClass().getSimpleName()); + super.onNewIntent(intent); + } + + protected void setTranslucentNavigation() { + boolean useNotch = PreferenceMgr.getBoolean(getContext(), PreferenceConstant.KEY_useNotch, true); + if (!useNotch) { + return; + } + //设置沉浸式虚拟键,在MIUI系统中,虚拟键背景透明。原生系统中,虚拟键背景半透明。 + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } + +// @Override +// public void startActivity(Intent intent) { +// try { +// if (intent.getComponent() != null) { +// Class c = Class.forName(intent.getComponent().getClassName()); +// if (c.getSuperclass() == BaseSlideActivity.class) { +// ActivityOptions compat = ActivityOptions.makeSceneTransitionAnimation(this); +// startActivity(new Intent(getContext(), c), compat.toBundle()); +// return; +// } +// } +// } catch (Exception e) { +// e.printStackTrace(); +// } +// super.startActivity(intent); +// } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { +// Timber.d("consume: activity(%s) onCreate start %s", getClass().getSimpleName(), (System.currentTimeMillis() - Application.start)); + Timber.d("onCreate===>%s", getClass().getSimpleName()); + checkForceDarkMode(getActivity()); + super.onCreate(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.getBoolean("recycleMe", false)) { + finish(); + return; + } + setContentView(initLayout(savedInstanceState)); +// Timber.d("consume: activity(%s) onCreate after setContentView %s", getClass().getSimpleName(), (System.currentTimeMillis() - Application.start)); + extraDataBundle = getIntent().getBundleExtra("extraDataBundle"); + if (drawStatusBar) { + MyStatusBarUtil.setColorNoTranslucent(this, getResources().getColor(R.color.white)); + } + initView(); + //以下代码用于去除阴影 + if (getSupportActionBar() != null) { + getSupportActionBar().setElevation(DisplayUtil.dpToPx(getContext(), 1) / 2); + } + initData(savedInstanceState); +// Timber.d("consume: activity(%s) onCreate end %s", getClass().getSimpleName(), (System.currentTimeMillis() - Application.start)); + } + + public static void checkForceDarkMode(Activity activity) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + TypedValue outValue = new TypedValue(); + activity.getTheme().resolveAttribute(android.R.attr.forceDarkAllowed, outValue, true); + if (outValue.data != 0) { + //开启了强制黑暗模式 + boolean forceDark = PreferenceMgr.getBoolean(activity, "forceDark", true); + if (!forceDark) { + if (activity.getWindow() != null) { + activity.getWindow().getDecorView().setForceDarkAllowed(false); + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + protected void onSaveInstanceState(@NotNull Bundle outState) { + if (finishWhenRestore()) { + outState.putBoolean("recycleMe", true); + super.onSaveInstanceState(outState); + } + } + + protected boolean finishWhenRestore() { + return false; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return item.getItemId() == android.R.id.home || super.onOptionsItemSelected(item); + } + + /** + * 初始化布局 + */ + protected abstract int initLayout(Bundle savedInstanceState); + + /** + * 初始化布局以及View控件 + */ + protected abstract void initView(); + + /** + * 处理业务逻辑,状态恢复等操作 + * + * @param savedInstanceState 鬼知道 + */ + protected abstract void initData(Bundle savedInstanceState); + + /** + * 查找View + * + * @param id 控件的id + * @param View类型 + * @return 鬼知道 + */ + protected VT findView(@IdRes int id) { + return (VT) findViewById(id); + } + + protected Context getContext() { + return this; + } + + protected Activity getActivity() { + return this; + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/base/BaseSlideActivity.java b/app/src/main/java/org/mozilla/xiu/browser/base/BaseSlideActivity.java new file mode 100644 index 0000000..7286b7c --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/base/BaseSlideActivity.java @@ -0,0 +1,134 @@ +package org.mozilla.xiu.browser.base; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; + +import org.mozilla.xiu.browser.R; +import org.mozilla.xiu.browser.utils.AndroidBarUtils; +import org.mozilla.xiu.browser.utils.DisplayUtil; + +/** + * 作者:By 15968 + * 日期:On 2021/1/23 + * 时间:At 22:07 + */ + +public abstract class BaseSlideActivity extends BaseActivity { + + // protected MyShadowBgAnimator shadowBgAnimator; + private boolean isFinished; + private View bgView; + + protected void clearFullScreen() { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + protected void showAnimation() { + if (bgView == null) { + return; + } + bgView.setAlpha(0f); + bgView.post(() -> { + float start = DisplayUtil.dpToPx(getContext(), 120); + float end = 0; + ObjectAnimator animator = ObjectAnimator.ofFloat(bgView, "translationY", start, end); + animator.setDuration(300); + animator.setInterpolator(new DecelerateInterpolator()); + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + bgView.setAlpha(1f); + } + + @Override + public void onAnimationEnd(Animator animation) { + + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }); + animator.start(); + ViewCompat.postInvalidateOnAnimation(bgView); + }); + } + + protected void showCloseAnimation() { + if (bgView == null) { + return; + } + float start = 0; + float end = DisplayUtil.dpToPx(getContext(), 120); + ObjectAnimator animator = ObjectAnimator.ofFloat(bgView, "translationY", start, end); + animator.setInterpolator(new AccelerateInterpolator()); + animator.setDuration(250); + animator.start(); + } + + protected abstract View getBackgroundView(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + drawStatusBar = false; + super.onCreate(savedInstanceState); + } + + @Override + protected void initView() { +// MyStatusBarUtil.setColorNoTranslucent(this, getResources().getColor(R.color.half_transparent)); + clearFullScreen(); + AndroidBarUtils.setTranslucentStatusBar3(this, true); + bgView = getBackgroundView(); + showAnimation(); +// shadowBgAnimator = new MyShadowBgAnimator(bgView); +// shadowBgAnimator.initAnimator(); + initView2(); + } + + +// @Override +// public void startActivity(Intent intent) { +// try { +// if (intent.getComponent() != null) { +// Class c = Class.forName(intent.getComponent().getClassName()); +// if (c.getSuperclass() == BaseSlideActivity.class) { +// ActivityOptions compat = ActivityOptions.makeSceneTransitionAnimation(this); +// startActivity(new Intent(getContext(), c), compat.toBundle()); +// return; +// } +// } +// } catch (Exception e) { +// e.printStackTrace(); +// } +// super.startActivity(intent); +// } + + protected abstract void initView2(); + + + @Override + public void finish() { + if (isFinished) { + return; + } + isFinished = true; + super.finish(); + showCloseAnimation(); + overridePendingTransition(0, R.anim.alpha_exit); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/MyEditText.java b/app/src/main/java/org/mozilla/xiu/browser/broswer/MyEditText.java new file mode 100644 index 0000000..d9be1f3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/MyEditText.java @@ -0,0 +1,40 @@ +package org.mozilla.xiu.browser.broswer; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class MyEditText extends androidx.appcompat.widget.AppCompatEditText { + + + + private Paint mPaint; + private int mViewHeight = 0; + private Rect mTextBound = new Rect(); + private LinearGradient mLinearGradient; + + public MyEditText(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + + @Override + protected void onDraw(Canvas canvas) { + mViewHeight = getMeasuredHeight(); + mPaint = getPaint(); + String mText = getText().toString(); + mPaint.getTextBounds(mText, 0, mText.length(), mTextBound); + mLinearGradient = new LinearGradient(0, 0, 0, mViewHeight,new int[]{0xFF8EDA4D, 0xFF4EB855}, null, Shader.TileMode.REPEAT); + mPaint.setShader(mLinearGradient); + canvas.drawText(mText, getMeasuredWidth() / 2 - mTextBound.width() / 2, getMeasuredHeight() / 2 + mTextBound.height() / 2, mPaint); + } + +} + diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/SearchEngine.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/SearchEngine.kt new file mode 100644 index 0000000..4c82610 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/SearchEngine.kt @@ -0,0 +1,21 @@ +package org.mozilla.xiu.browser.broswer + +import android.app.Activity +import androidx.preference.PreferenceManager +import org.mozilla.xiu.browser.R + +fun SearchEngine(activity: Activity):String { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity /* Activity context */) + var engine:String + engine = if (sharedPreferences.getBoolean("switch_diy",false)) + sharedPreferences.getString("edit_diy","").toString() + else + sharedPreferences.getString("searchEngine",activity.getString(R.string.bing)).toString() + sharedPreferences.registerOnSharedPreferenceChangeListener{ sharedPreferences, _ -> + engine = if (sharedPreferences.getBoolean("switch_diy",false)) + sharedPreferences.getString("edit_diy","").toString() + else + sharedPreferences.getString("searchEngine",activity.getString(R.string.bing)).toString() + } + return engine +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkAdapter.kt new file mode 100644 index 0000000..5144ea7 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkAdapter.kt @@ -0,0 +1,78 @@ +package org.mozilla.xiu.browser.broswer.bookmark + +import android.content.Context +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.broswer.history.HistoryAdapter +import org.mozilla.xiu.browser.database.bookmark.Bookmark +import org.mozilla.xiu.browser.databinding.ItemBookmarkBinding + +class BookmarkAdapter : ListAdapter( +BookmarkListCallback +) { + lateinit var select: Select + lateinit var popupSelect: PopupSelect + + inner class ItemTestViewHolder(private val binding: ItemBookmarkBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: Bookmark, mContext: Context){ + + binding.textView9.text=bean.title + binding.textView10.text=bean.url + binding.bookmarkItem.setOnClickListener { bean.url?.let { it1 -> select.onSelect(it1) } } + binding.materialButton18.setOnClickListener { + showMenu(it,bean,mContext) + false + } + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemBookmarkBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + interface Select{ + fun onSelect(url: String) + } + interface PopupSelect{ + fun onPopupSelect(bean: Bookmark, item:Int) + } + + private fun showMenu(v: View, bean: Bookmark, context: Context) { + val popup = PopupMenu(context!!, v) + popup.menuInflater.inflate(R.menu.bookmark_item_menu, popup.menu) + + popup.setOnMenuItemClickListener { menuItem: MenuItem -> + // Respond to menu item click. + when(menuItem.itemId){ + R.id.menu_bookmark_item_delete -> { + popupSelect.onPopupSelect(bean, HistoryAdapter.DELETE) + + } + R.id.menu_bookmark_item_add_home ->{ + popupSelect.onPopupSelect(bean, HistoryAdapter.ADD_TO_HOMEPAGE) + + } + } + false + } + popup.setOnDismissListener { + // Respond to popup being dismissed. + } + // Show the popup menu. + popup.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkFragment.kt new file mode 100644 index 0000000..ed319cc --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkFragment.kt @@ -0,0 +1,111 @@ +package org.mozilla.xiu.browser.broswer.bookmark + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager + +import org.mozilla.xiu.browser.broswer.history.HistoryAdapter +import org.mozilla.xiu.browser.database.bookmark.Bookmark +import org.mozilla.xiu.browser.database.bookmark.BookmarkViewModel +import org.mozilla.xiu.browser.database.shortcut.Shortcut +import org.mozilla.xiu.browser.database.shortcut.ShortcutViewModel +import org.mozilla.xiu.browser.databinding.FragmentBookmarkBinding +import org.mozilla.xiu.browser.session.createSession +import org.mozilla.xiu.browser.utils.GroupUtils + +/** + * A simple [Fragment] subclass. + * Use the [BookmarkFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class BookmarkFragment : Fragment() { + lateinit var binding:FragmentBookmarkBinding + private lateinit var bookmarkViewModel: BookmarkViewModel + private lateinit var shortcutViewModel: ShortcutViewModel + private var bookmarks: List? = null + var i:Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bookmarkViewModel = ViewModelProvider(requireActivity()).get(BookmarkViewModel::class.java) + shortcutViewModel = ViewModelProvider(this)[ShortcutViewModel::class.java] + + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding=FragmentBookmarkBinding.inflate(LayoutInflater.from(requireContext())) + + var bookmarkAdapter= BookmarkAdapter() + binding.bookmarkRecyclerview.adapter=bookmarkAdapter + binding.bookmarkRecyclerview.layoutManager = LinearLayoutManager(context) + + binding.BookmarkFragmentEdittext?.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + //var s1=s.toString().trim() + bookmarkViewModel.findBookmarksWithPattern(s.toString())?.observe(viewLifecycleOwner){ + bookmarkAdapter.submitList(it) + } + + + + } + + override fun afterTextChanged(s: Editable?) { + + } + }) + + + bookmarkViewModel.allBookmarksLive?.observe(requireActivity()){ + if (i == 0) { + val groupUtils= it?.let { it1 -> GroupUtils(it1) } + if (groupUtils != null) { + bookmarks = groupUtils.groupBookmark() + bookmarkAdapter.submitList(groupUtils.groupBookmark()) + } + i=1 + } + + } + + bookmarkAdapter.select= object : BookmarkAdapter.Select { + override fun onSelect(url: String) { + createSession(url,requireActivity()) + } + + } + bookmarkAdapter.popupSelect = object : BookmarkAdapter.PopupSelect { + override fun onPopupSelect(bean: Bookmark, item: Int) { + when(item){ + HistoryAdapter.DELETE ->{ + bookmarkViewModel.deleteBookmarks(bean) + bookmarks = bookmarks?.toMutableList()?.apply { remove(bean) } + bookmarkAdapter.submitList(bookmarks) + + } + HistoryAdapter.ADD_TO_HOMEPAGE ->{ + shortcutViewModel.insertShortcuts(Shortcut(bean.url,bean.title,System.currentTimeMillis().toInt())) + } + } + } + + } + + return binding.root + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkListCallback.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkListCallback.kt new file mode 100644 index 0000000..2dd24a3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/BookmarkListCallback.kt @@ -0,0 +1,18 @@ +package org.mozilla.xiu.browser.broswer.bookmark + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import org.mozilla.xiu.browser.database.bookmark.Bookmark + +object BookmarkListCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { + return oldItem ==newItem + + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { + return oldItem ==newItem + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/shortcut/ShortcutAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/shortcut/ShortcutAdapter.kt new file mode 100644 index 0000000..3ef1d0e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/shortcut/ShortcutAdapter.kt @@ -0,0 +1,66 @@ +package org.mozilla.xiu.browser.broswer.bookmark.shortcut + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.database.shortcut.Shortcut +import org.mozilla.xiu.browser.databinding.ItemShortcutBinding +import java.net.URI + +class ShortcutAdapter : ListAdapter( +ShortcutListCallback +) { + lateinit var select: Select + lateinit var longClick: LongClick + + inner class ItemTestViewHolder(private val binding: ItemShortcutBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: Shortcut, mContext: Context){ + + binding.textView21.text=bean.title + val uri = URI.create(bean.url) + val faviconUrl = uri.scheme + "://" + uri.host + "/favicon.ico" + Glide.with(mContext) + .load(faviconUrl) + .placeholder(R.drawable.globe) + .into(binding.imageView10) + binding.materialCardView8.setOnClickListener { bean.url?.let { it1 -> select.onSelect(it1) } } + binding.materialCardView8.setOnLongClickListener { + dialog(mContext,bean) + false + } + + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemShortcutBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + } + + interface Select{ + fun onSelect(url: String) + } + interface LongClick{ + fun onLongClick(bean: Shortcut) + } + private fun dialog(context: Context,bean: Shortcut){ + MaterialAlertDialogBuilder(context) + .setTitle(context.getString(R.string.dialog_shortcut_title)) + .setNegativeButton(context.getString(R.string.cancel)) { _, _ -> } + .setPositiveButton(context.getString(R.string.confirm)) { dialog, which -> + longClick.onLongClick(bean) + } + .show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/shortcut/ShortcutListCallback.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/shortcut/ShortcutListCallback.kt new file mode 100644 index 0000000..8ee42e6 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/shortcut/ShortcutListCallback.kt @@ -0,0 +1,18 @@ +package org.mozilla.xiu.browser.broswer.bookmark.shortcut + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import org.mozilla.xiu.browser.database.shortcut.Shortcut + +object ShortcutListCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Shortcut, newItem: Shortcut): Boolean { + return oldItem ==newItem + + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: Shortcut, newItem: Shortcut): Boolean { + return oldItem ==newItem + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFolderAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFolderAdapter.kt new file mode 100644 index 0000000..38672c1 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFolderAdapter.kt @@ -0,0 +1,44 @@ +package org.mozilla.xiu.browser.broswer.bookmark.sync + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.databinding.ItemSyncBookmarkFolderBinding +import mozilla.components.concept.storage.BookmarkNode + +class SyncBookmarkFolderAdapter : ListAdapter( + SyncBookmarkListCallback +) { + lateinit var select: Select + + inner class ItemTestViewHolder(private val binding: ItemSyncBookmarkFolderBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: BookmarkNode, mContext: Context){ + binding.textView4.text= + bean.title + ?.replace("toolbar","工具栏书签") + ?.replace("menu","菜单书签") + ?.replace("mobile","移动设备书签") + ?.replace("root","所有书签") + //?.replace("unfiled","所有书签") + // binding.textView10.text=bean.url + binding.root.setOnClickListener { select.onSelect(bean) } + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemSyncBookmarkFolderBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + interface Select{ + fun onSelect(bean: BookmarkNode) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFolderFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFolderFragment.kt new file mode 100644 index 0000000..54477b4 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFolderFragment.kt @@ -0,0 +1,32 @@ +package org.mozilla.xiu.browser.broswer.bookmark.sync + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.mozilla.xiu.browser.R + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val GUID = "guid" + +class SyncBookmarkFolderFragment : Fragment() { + // TODO: Rename and change types of parameters + private var guid: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_sync_bookmark_folder, container, false) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFragment.kt new file mode 100644 index 0000000..35eab9b --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkFragment.kt @@ -0,0 +1,110 @@ +package org.mozilla.xiu.browser.broswer.bookmark.sync + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.databinding.FragmentSyncBookmarkBinding +import org.mozilla.xiu.browser.session.GeckoViewModel +import org.mozilla.xiu.browser.session.SeRuSettings +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.browser.storage.sync.PlacesBookmarksStorage +import mozilla.components.concept.storage.BookmarkNode +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession + +/** + * A simple [Fragment] subclass. + * Use the [SyncBookmarkFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class SyncBookmarkFragment : Fragment() { + + lateinit var binding: FragmentSyncBookmarkBinding + lateinit var bookmarkNodes:ArrayList + lateinit var geckoViewModel: GeckoViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + bookmarkNodes=ArrayList() + + val bookmarksStorage = lazy { + PlacesBookmarksStorage(this.requireContext()) + } + GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage) + binding= FragmentSyncBookmarkBinding.inflate(LayoutInflater.from(context)) + var bookmarkAdapter= SyncBookmarkFolderAdapter() + geckoViewModel = activity?.let { ViewModelProvider(it)[GeckoViewModel::class.java] }!! + + lifecycleScope.launch { + binding.syncBookmarkRecyclerView.adapter=bookmarkAdapter + binding.syncBookmarkRecyclerView.layoutManager = LinearLayoutManager(context) + bookmarkAdapter.select= object : SyncBookmarkFolderAdapter.Select { + override fun onSelect(bean: BookmarkNode) { + //createSession(url) + val bundle = Bundle() + bundle.putString("guid", bean.guid) + findNavController().navigate(R.id.action_syncBookmarkFragment_to_syncBookmarkListFragment,bundle) + } + + } + + + + bookmarkAdapter.submitList(withContext(Dispatchers.IO) { + val bookmarksRoot = + bookmarksStorage.value?.getTree("root________", recursive = true) + if (bookmarksRoot == null) { + bookmarkNodes + } else { + var bookmarksRootAndChildren = "BOOKMARKS\n" + fun addTreeNode(node: BookmarkNode, depth: Int) { + Log.d("BookmarkNode: ", node.type.name) + if(node.type.name == "FOLDER") + bookmarkNodes.add(node) + node.children?.forEach { + addTreeNode(it, depth + 1) + } + } + addTreeNode(bookmarksRoot, 0) + bookmarkNodes + + } + }.toList()) + + } + + return binding.root + } + + fun createSession(uri: String) { + val session = GeckoSession() + val sessionSettings = session.settings + SeRuSettings(sessionSettings, requireActivity()) + + context?.let { GeckoRuntime.getDefault(it) }?.let { session.open(it) } + session.loadUri(uri) + geckoViewModel.changeSearch(session) + HomeLivedata.getInstance().Value(false) + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkItemAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkItemAdapter.kt new file mode 100644 index 0000000..52054cc --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkItemAdapter.kt @@ -0,0 +1,54 @@ +package org.mozilla.xiu.browser.broswer.bookmark.sync + +import android.content.Context +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.database.bookmark.Bookmark +import org.mozilla.xiu.browser.databinding.ItemBookmarkBinding +import mozilla.components.concept.storage.BookmarkNode + +class SyncBookmarkItemAdapter: ListAdapter( + SyncBookmarkListCallback +) { + lateinit var select: Select + lateinit var popupSelect: PopupSelect + + inner class ItemTestViewHolder(private val binding: ItemBookmarkBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: BookmarkNode, mContext: Context){ + + binding.textView9.text=bean.title + bean.parentGuid?.let { Log.d("BookmarkNode1", it) } + + binding.textView10.text=bean.url + binding.bookmarkItem.setOnClickListener { bean.url?.let { it1 -> select.onSelect(it1) } } + binding.materialButton18.visibility = View.GONE + + + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemBookmarkBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + interface Select{ + fun onSelect(url: String) + } + interface PopupSelect{ + fun onPopupSelect(bean: Bookmark, item:Int) + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkListCallback.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkListCallback.kt new file mode 100644 index 0000000..3f6ef2c --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkListCallback.kt @@ -0,0 +1,20 @@ +package org.mozilla.xiu.browser.broswer.bookmark.sync + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import mozilla.components.concept.storage.BookmarkNode + +object SyncBookmarkListCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: BookmarkNode, newItem: BookmarkNode): Boolean { + return oldItem.title == newItem.title + && oldItem.url == newItem.url + + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: BookmarkNode, newItem: BookmarkNode): Boolean { + return oldItem.title == newItem.title + && oldItem.url == newItem.url + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkListFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkListFragment.kt new file mode 100644 index 0000000..aef955a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/bookmark/sync/SyncBookmarkListFragment.kt @@ -0,0 +1,96 @@ +package org.mozilla.xiu.browser.broswer.bookmark.sync + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.databinding.FragmentSyncBookmarkListBinding +import org.mozilla.xiu.browser.session.createSession +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.browser.storage.sync.PlacesBookmarksStorage +import mozilla.components.concept.storage.BookmarkNode +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider + + +class SyncBookmarkListFragment : Fragment() { + // TODO: Rename and change types of parameters + private var guid: String? = null + private lateinit var bookmarkNodes:ArrayList + lateinit var binding:FragmentSyncBookmarkListBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + guid = arguments?.getString("guid") + bookmarkNodes=ArrayList() + binding = FragmentSyncBookmarkListBinding.inflate(LayoutInflater.from(requireContext())) + + //guid?.let { Log.d("arguments?.getString", it) } + + + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + var syncBookmarkItemAdapter= SyncBookmarkItemAdapter() + val bookmarksStorage = lazy { + PlacesBookmarksStorage(this.requireContext()) + } + + binding.constraintLayout16.setOnClickListener { + findNavController().navigate(R.id.action_syncBookmarkListFragment_to_syncBookmarkFragment) + } + + GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage) + lifecycleScope.launch { + binding.recyclerView5.adapter=syncBookmarkItemAdapter + binding.recyclerView5.layoutManager = LinearLayoutManager(context) + syncBookmarkItemAdapter.select= object : SyncBookmarkItemAdapter.Select { + override fun onSelect(url: String) { + createSession(url,requireActivity()) + + } + + } + + + + syncBookmarkItemAdapter.submitList(withContext(Dispatchers.IO) { + val bookmarksRoot = + guid?.let { bookmarksStorage.value?.getTree(it, recursive = true) } + if (bookmarksRoot == null) { + bookmarkNodes + } else { + var bookmarksRootAndChildren = "BOOKMARKS\n" + fun addTreeNode(node: BookmarkNode, depth: Int) { + Log.d("BookmarkNode: ", node.type.name) + if(node.type.name == "ITEM") + bookmarkNodes.add(node) + node.children?.forEach { + addTreeNode(it, depth + 1) + } + } + addTreeNode(bookmarksRoot, 0) + bookmarkNodes + + } + }.toList()) + + } + + return binding.root + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/AlertDialog.java b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/AlertDialog.java new file mode 100644 index 0000000..1c5eaa5 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/AlertDialog.java @@ -0,0 +1,80 @@ +package org.mozilla.xiu.browser.broswer.dialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import org.mozilla.xiu.browser.R; + +import org.mozilla.geckoview.GeckoSession; + +public class AlertDialog extends androidx.appcompat.app.AlertDialog { + GeckoSession.PromptDelegate.PromptResponse dialogResult; + Handler mHandler ; + Context context; + public AlertDialog(@NonNull Context context, GeckoSession.PromptDelegate.AlertPrompt alertPrompt) { + super(context); + this.context=context; + onCreate(alertPrompt,context); + } + public void onCreate(GeckoSession.PromptDelegate.AlertPrompt alertPrompt, Context context) { + setTitle(alertPrompt.title); + setMessage(alertPrompt.message); + setButton(BUTTON_POSITIVE, "确认", new OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + endDialog(alertPrompt.dismiss()); + } + }); + + + } + public void endDialog(GeckoSession.PromptDelegate.PromptResponse result) + { + setDialogResult(result); + super.dismiss(); + Message m = mHandler.obtainMessage(); + mHandler.sendMessage(m); + Log.d("endDia",result+""); + + + + + + } + @SuppressLint("HandlerLeak") + public GeckoSession.PromptDelegate.PromptResponse showDialog() + { + mHandler = new Handler() { + @Override + public void handleMessage(Message mesg) { + // process incoming messages here + //super.handleMessage(msg); + throw new RuntimeException(); + } + }; + super.show(); + try { + Looper.getMainLooper().loop(); + } + catch(RuntimeException e2) + { + } + return dialogResult; + } + + public GeckoSession.PromptDelegate.PromptResponse getDialogResult() { + return dialogResult; + } + + public void setDialogResult(GeckoSession.PromptDelegate.PromptResponse dialogResult) { + this.dialogResult = dialogResult; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/ButtonDialog.java b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/ButtonDialog.java new file mode 100644 index 0000000..5c53972 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/ButtonDialog.java @@ -0,0 +1,90 @@ +package org.mozilla.xiu.browser.broswer.dialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.mozilla.geckoview.GeckoSession; + +public class ButtonDialog extends androidx.appcompat.app.AlertDialog { + GeckoSession.PromptDelegate.PromptResponse dialogResult; + Handler mHandler ; + Context context; + public ButtonDialog(@NonNull Context context, GeckoSession.PromptDelegate.ButtonPrompt alertPrompt) { + super(context); + this.context=context; + onCreate(alertPrompt,context); + } + public void onCreate(GeckoSession.PromptDelegate.ButtonPrompt alertPrompt, Context context) { + setTitle(alertPrompt.title); + setMessage(alertPrompt.message); + setButton(BUTTON_POSITIVE, "确认", new OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + endDialog(alertPrompt.confirm(GeckoSession.PromptDelegate.ButtonPrompt.Type.POSITIVE)); + } + }); + setButton(BUTTON_NEGATIVE, "取消", new OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + endDialog(alertPrompt.confirm(GeckoSession.PromptDelegate.ButtonPrompt.Type.NEGATIVE)); + } + }); + setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + endDialog(alertPrompt.confirm(GeckoSession.PromptDelegate.ButtonPrompt.Type.NEGATIVE)); + + } + }); + + + } + public void endDialog(GeckoSession.PromptDelegate.PromptResponse result) + { + setDialogResult(result); + super.dismiss(); + Message m = mHandler.obtainMessage(); + mHandler.sendMessage(m); + Log.d("endDia",result+""); + + + + + + } + @SuppressLint("HandlerLeak") + public GeckoSession.PromptDelegate.PromptResponse showDialog() + { + mHandler = new Handler() { + @Override + public void handleMessage(Message mesg) { + // process incoming messages here + //super.handleMessage(msg); + throw new RuntimeException(); + } + }; + super.show(); + try { + Looper.getMainLooper().loop(); + } + catch(RuntimeException e2) + { + } + return dialogResult; + } + + public GeckoSession.PromptDelegate.PromptResponse getDialogResult() { + return dialogResult; + } + + public void setDialogResult(GeckoSession.PromptDelegate.PromptResponse dialogResult) { + this.dialogResult = dialogResult; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/ConfirmDialog.java b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/ConfirmDialog.java new file mode 100644 index 0000000..653504a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/ConfirmDialog.java @@ -0,0 +1,77 @@ +package org.mozilla.xiu.browser.broswer.dialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.mozilla.geckoview.GeckoSession; + +public class ConfirmDialog extends androidx.appcompat.app.AlertDialog { + GeckoSession.PromptDelegate.PromptResponse dialogResult; + Handler mHandler ; + Context context; + public ConfirmDialog(@NonNull Context context, GeckoSession.PromptDelegate.RepostConfirmPrompt repostConfirmPrompt) { + super(context); + this.context=context; + onCreate(repostConfirmPrompt,context); + } + public void onCreate(GeckoSession.PromptDelegate.RepostConfirmPrompt repostConfirmPrompt, Context context) { + setTitle(repostConfirmPrompt.title); + //setMessage(repostConfirmPrompt.message); + setButton(BUTTON_POSITIVE, "确认", new OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + endDialog(repostConfirmPrompt.dismiss()); + } + }); + + + } + public void endDialog(GeckoSession.PromptDelegate.PromptResponse result) + { + setDialogResult(result); + super.dismiss(); + Message m = mHandler.obtainMessage(); + mHandler.sendMessage(m); + Log.d("endDia",result+""); + + + + + + } + @SuppressLint("HandlerLeak") + public GeckoSession.PromptDelegate.PromptResponse showDialog() + { + mHandler = new Handler() { + @Override + public void handleMessage(Message mesg) { + // process incoming messages here + //super.handleMessage(msg); + throw new RuntimeException(); + } + }; + super.show(); + try { + Looper.getMainLooper().loop(); + } + catch(RuntimeException e2) + { + } + return dialogResult; + } + + public GeckoSession.PromptDelegate.PromptResponse getDialogResult() { + return dialogResult; + } + + public void setDialogResult(GeckoSession.PromptDelegate.PromptResponse dialogResult) { + this.dialogResult = dialogResult; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/JsChoiceDialog.java b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/JsChoiceDialog.java new file mode 100644 index 0000000..b9311e5 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/JsChoiceDialog.java @@ -0,0 +1,111 @@ +package org.mozilla.xiu.browser.broswer.dialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import androidx.annotation.NonNull; + +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.xiu.browser.componets.MyDialog; +import org.mozilla.xiu.browser.databinding.DiaChoiceBinding; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class JsChoiceDialog extends MyDialog { + int dialogResult; + Handler mHandler ; + DiaChoiceBinding binding; + public JsChoiceDialog(@NonNull Context context, GeckoSession.PromptDelegate.ChoicePrompt choicePrompt) { + super(context); + onCreate(choicePrompt,context); + } + public void onCreate(GeckoSession.PromptDelegate.ChoicePrompt choicePrompt, Context context) { + binding= DiaChoiceBinding.inflate(LayoutInflater.from(getContext())); + + setTitle(choicePrompt.title); + setMessage(choicePrompt.message); + setView(binding.getRoot()); + List collect= null; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + collect = Arrays.stream(choicePrompt.choices).map(choice -> choice.label).collect(Collectors.toList()); + } + for (int i=0;i + + when (findViewById(checkedId)) { + binding.radioButton1 -> { + prefs?.edit()?.putString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.baidu) + ) + ?.commit() + prefs?.edit()?.putBoolean("switch_diy", false)?.commit() + } + + binding.radioButton2 -> { + prefs?.edit()?.putString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.google) + ) + ?.commit() + prefs?.edit()?.putBoolean("switch_diy", false)?.commit() + } + + binding.radioButton3 -> { + prefs?.edit()?.putString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.bing) + ) + ?.commit() + prefs?.edit()?.putBoolean("switch_diy", false)?.commit() + } + + binding.radioButton4 -> { + prefs?.edit()?.putString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.sogou) + ) + ?.commit() + prefs?.edit()?.putBoolean("switch_diy", false)?.commit() + } + + binding.radioButton6 -> { + prefs?.edit()?.putString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.sk360) + ) + ?.commit() + prefs?.edit()?.putBoolean("switch_diy", false)?.commit() + } + + binding.radioButton7 -> { + prefs?.edit()?.putString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.wuzhui) + ) + ?.commit() + prefs?.edit()?.putBoolean("switch_diy", false)?.commit() + } + + binding.radioButton8 -> { + prefs?.edit()?.putString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.yandex) + ) + ?.commit() + prefs?.edit()?.putBoolean("switch_diy", false)?.commit() + } + + binding.radioButton9 -> { + prefs?.edit()?.putString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.shenma) + ) + ?.commit() + prefs?.edit()?.putBoolean("switch_diy", false)?.commit() + } + } + dismiss() + } + + if (prefs != null) { + when (prefs.getString( + "searchEngine", + getContext().getString(org.mozilla.xiu.browser.R.string.baidu) + )) { + getContext().getString(org.mozilla.xiu.browser.R.string.baidu) -> binding.radioButton1.isChecked = + true + + getContext().getString(org.mozilla.xiu.browser.R.string.google) -> binding.radioButton2.isChecked = + true + + getContext().getString(org.mozilla.xiu.browser.R.string.bing) -> binding.radioButton3.isChecked = + true + + getContext().getString(org.mozilla.xiu.browser.R.string.sogou) -> binding.radioButton4.isChecked = + true + + getContext().getString(org.mozilla.xiu.browser.R.string.sk360) -> binding.radioButton6.isChecked = + true + + getContext().getString(org.mozilla.xiu.browser.R.string.wuzhui) -> binding.radioButton7.isChecked = + true + + getContext().getString(org.mozilla.xiu.browser.R.string.yandex) -> binding.radioButton8.isChecked = + true + + getContext().getString(org.mozilla.xiu.browser.R.string.shenma) -> binding.radioButton9.isChecked = + true + } + } + if (isDiy == true) { + binding.radioButton5.visibility = View.VISIBLE + binding.radioButton5.isChecked = true + } else + binding.radioButton5.visibility = View.GONE + setView(binding.root) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/TextDialog.java b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/TextDialog.java new file mode 100644 index 0000000..d854f14 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/dialog/TextDialog.java @@ -0,0 +1,78 @@ +package org.mozilla.xiu.browser.broswer.dialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.mozilla.geckoview.GeckoSession; + +public class TextDialog extends androidx.appcompat.app.AlertDialog { + GeckoSession.PromptDelegate.PromptResponse dialogResult; + Handler mHandler ; + Context context; + public TextDialog(@NonNull Context context, GeckoSession.PromptDelegate.TextPrompt alertPrompt) { + super(context); + this.context=context; + onCreate(alertPrompt,context); + } + public void onCreate(GeckoSession.PromptDelegate.TextPrompt alertPrompt, Context context) { + setTitle(alertPrompt.title); + setMessage(alertPrompt.message); + setButton(BUTTON_POSITIVE, "确认", new OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + endDialog(alertPrompt.dismiss()); + } + }); + + + + } + public void endDialog(GeckoSession.PromptDelegate.PromptResponse result) + { + setDialogResult(result); + super.dismiss(); + Message m = mHandler.obtainMessage(); + mHandler.sendMessage(m); + Log.d("endDia",result+""); + + + + + + } + @SuppressLint("HandlerLeak") + public GeckoSession.PromptDelegate.PromptResponse showDialog() + { + mHandler = new Handler() { + @Override + public void handleMessage(Message mesg) { + // process incoming messages here + //super.handleMessage(msg); + throw new RuntimeException(); + } + }; + super.show(); + try { + Looper.getMainLooper().loop(); + } + catch(RuntimeException e2) + { + } + return dialogResult; + } + + public GeckoSession.PromptDelegate.PromptResponse getDialogResult() { + return dialogResult; + } + + public void setDialogResult(GeckoSession.PromptDelegate.PromptResponse dialogResult) { + this.dialogResult = dialogResult; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryAdapter.kt new file mode 100644 index 0000000..78e8b2f --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryAdapter.kt @@ -0,0 +1,74 @@ +package org.mozilla.xiu.browser.broswer.history + +import android.content.Context +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.database.history.History +import org.mozilla.xiu.browser.databinding.ItemBookmarkBinding + +class HistoryAdapter : ListAdapter(HistoryListCallback) { + lateinit var select: Select + lateinit var popupSelect: PopupSelect + companion object { + var DELETE = 0 + var ADD_TO_HOMEPAGE = 1 + } + + inner class ItemTestViewHolder(private val binding: ItemBookmarkBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: History, mContext: Context){ + binding.textView9.text=bean.title + binding.textView10.text=bean.url + binding.bookmarkItem.setOnClickListener { bean.url?.let { it1 -> select.onSelect(it1) } } + binding.materialButton18.setOnClickListener { v -> showMenu(v,bean,mContext) } + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemBookmarkBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + interface Select{ + fun onSelect(url: String) + } + interface PopupSelect{ + fun onPopupSelect(bean: History,item:Int) + } + + private fun showMenu(v: View, bean: History,context: Context) { + val popup = PopupMenu(context!!, v) + popup.menuInflater.inflate(R.menu.bookmark_item_menu, popup.menu) + + popup.setOnMenuItemClickListener { menuItem: MenuItem -> + // Respond to menu item click. + when(menuItem.itemId){ + R.id.menu_bookmark_item_delete -> { + popupSelect.onPopupSelect(bean, DELETE) + + } + R.id.menu_bookmark_item_add_home ->{ + popupSelect.onPopupSelect(bean, ADD_TO_HOMEPAGE) + + } + } + false + } + popup.setOnDismissListener { + // Respond to popup being dismissed. + } + // Show the popup menu. + popup.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryFragment.kt new file mode 100644 index 0000000..fd6b7b3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryFragment.kt @@ -0,0 +1,128 @@ +package org.mozilla.xiu.browser.broswer.history + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.database.history.History +import org.mozilla.xiu.browser.database.history.HistoryViewModel +import org.mozilla.xiu.browser.database.shortcut.Shortcut +import org.mozilla.xiu.browser.database.shortcut.ShortcutViewModel +import org.mozilla.xiu.browser.databinding.FragmentHistoryBinding +import org.mozilla.xiu.browser.session.createSession + +class HistoryFragment : Fragment() { + lateinit var binding:FragmentHistoryBinding + lateinit var historyViewModel: HistoryViewModel + lateinit var shortcutViewModel: ShortcutViewModel + private var histories: List? = null + lateinit var historyAdapter: HistoryAdapter + var i:Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java] + shortcutViewModel = ViewModelProvider(this)[ShortcutViewModel::class.java] + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + binding=FragmentHistoryBinding.inflate(LayoutInflater.from(requireContext())) + historyAdapter= HistoryAdapter() + binding.historyRecyclerview.adapter=historyAdapter + binding.historyRecyclerview.layoutManager = LinearLayoutManager(context) + historyViewModel.allHistoriesLive?.observe(viewLifecycleOwner){ + if (i == 0) { + histories = it + historyAdapter.submitList(histories) + i=1 + } + + + } + + + + binding.HistoryFragmentSearching?.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + var s1=s.toString().trim() + historyViewModel.findHistoriesWithMix(s1)?.observe(viewLifecycleOwner){ + historyAdapter.submitList(it) + } + + + + } + + override fun afterTextChanged(s: Editable?) { + + } + }) + + + binding.materialButton20.setOnClickListener { showMenu(it) } + + + + historyAdapter.select= object : HistoryAdapter.Select { + override fun onSelect(url: String) { + createSession(url,requireActivity()) + } + + } + historyAdapter.popupSelect = object : HistoryAdapter.PopupSelect { + override fun onPopupSelect(bean: History, item: Int) { + when(item){ + HistoryAdapter.DELETE ->{ + historyViewModel.deleteHistories(bean) + histories = histories?.toMutableList()?.apply { remove(bean) } + historyAdapter.submitList(histories) + + } + HistoryAdapter.ADD_TO_HOMEPAGE ->{ + shortcutViewModel.insertShortcuts(Shortcut(bean.url,bean.title,System.currentTimeMillis().toInt())) + } + } + } + + } + + return binding.root + } + + private fun showMenu(v: View) { + val popup = PopupMenu(requireContext(), v) + popup.menuInflater.inflate(R.menu.history_menu, popup.menu) + + popup.setOnMenuItemClickListener { menuItem: MenuItem -> + // Respond to menu item click. + when(menuItem.itemId){ + R.id.history_menu_all_delete ->{ + historyViewModel.deleteAllHistories() + historyAdapter.submitList(null) + } + } + false + } + popup.setOnDismissListener { + // Respond to popup being dismissed. + } + // Show the popup menu. + popup.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryListCallback.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryListCallback.kt new file mode 100644 index 0000000..6f6bf75 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/history/HistoryListCallback.kt @@ -0,0 +1,18 @@ +package org.mozilla.xiu.browser.broswer.history + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import org.mozilla.xiu.browser.database.history.History + +object HistoryListCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: History, newItem: History): Boolean { + return oldItem ==newItem + + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: History, newItem: History): Boolean { + return oldItem ==newItem + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/home/TipsAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/home/TipsAdapter.kt new file mode 100644 index 0000000..0053151 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/home/TipsAdapter.kt @@ -0,0 +1,40 @@ +package org.mozilla.xiu.browser.broswer.home + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.broswer.history.HistoryListCallback +import org.mozilla.xiu.browser.database.history.History +import org.mozilla.xiu.browser.databinding.ItemSearchingTipsBinding + +class TipsAdapter : ListAdapter(HistoryListCallback) { + lateinit var select: Select + + inner class ItemTestViewHolder(private val binding: ItemSearchingTipsBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: History, mContext: Context){ + binding.textView26.text=bean.title + binding.textView27.text=bean.url + binding.tipsItem.setOnClickListener { select.onSelect(bean.url) } + + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemSearchingTipsBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + interface Select{ + fun onSelect(url: String) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/broswer/qr/QrScanningFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/broswer/qr/QrScanningFragment.kt new file mode 100644 index 0000000..0eabd46 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/broswer/qr/QrScanningFragment.kt @@ -0,0 +1,402 @@ +package org.mozilla.xiu.browser.broswer.qr + +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.media.Image +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.camera.core.CameraControl +import androidx.camera.core.CameraSelector +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.FocusMeteringAction +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture +import androidx.camera.core.Preview +import androidx.camera.core.SurfaceOrientedMeteringPointFactory +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.core.view.WindowCompat +import androidx.fragment.app.Fragment +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import org.mozilla.xiu.browser.MainActivity +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.session.createSession +import org.mozilla.xiu.browser.utils.Utils.playVibrate +import org.mozilla.xiu.browser.utils.Utils.requireColor +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@ExperimentalGetImage class QrScanningFragment: Fragment() { + + var width :Float = 0f + var height :Float = 0f + private var scaleY :Float = 0f + private var scaleX :Float = 0f + @ExperimentalAnimationApi + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View= ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false) + + setContent { + + var x by remember { + mutableStateOf((this.width/2).toFloat()) + } + var y by remember { + mutableStateOf((this.height/2).toFloat()) + } + var enableTorch by remember { + mutableStateOf(false) + } + var scan by remember { + mutableStateOf(false) + } + val openDialog = remember { + mutableStateOf(false) + } + var result by remember { + mutableStateOf("") + } + + + val imageAnalysis = ImageAnalysis + .Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + imageAnalysis.setAnalyzer( + ContextCompat.getMainExecutor(requireContext()) + ) { it -> + val mediaImage: Image? = it.image + + if (mediaImage != null) { + // 创建InputImage对象 + val inputImage: InputImage = InputImage.fromMediaImage( + mediaImage, + it.imageInfo.rotationDegrees + ) + + // 使用BarcodeScanner识别二维码 + val barcodeScanner = BarcodeScanning.getClient(); + initScale(it.width,it.height) + + barcodeScanner + .process(inputImage) + .addOnSuccessListener { barcodes -> + if (barcodes.isNotEmpty()){ + val barcode = barcodes[0] + barcode.boundingBox.let {rect -> + if (rect != null) { + x = rect.centerX().toFloat()-rect.width() + y = rect.centerY().toFloat()+rect.height()*3/2 + + } + + } + + + if (barcode.format === Barcode.FORMAT_QR_CODE) { + playVibrate(requireContext(),false) + barcodeScanner.close() + it.close() + + Handler().postDelayed({ + openDialog.value = true + result = barcode.url?.url!! + }, 1000) + /*val intent = Intent(requireContext(), MainActivity::class.java) + if ((getResources().getConfiguration().screenLayout and + Configuration.SCREENLAYOUT_SIZE_MASK) === + Configuration.SCREENLAYOUT_SIZE_LARGE) + { + createSession(barcode.url?.url,requireActivity()) + }else { + intent.data = Uri.parse(barcode.url?.url) + intent.flags= Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + }*/ + + } + } + + + + scan = barcodes.size != 0 + + } + .addOnFailureListener { e -> + // 处理扫描失败的情况 + Log.e("QrScanActivity", "Failed to scan barcode", e) + } + .addOnCompleteListener { task -> it.close() + } + } else { + it.close() + Log.d("QrScanActivity", "Failed to scan barcode") + + } + } + Box(contentAlignment = Alignment.BottomCenter) { + CameraView(preview = androidx.camera.core.Preview.Builder().build(), imageAnalysis = imageAnalysis, enableTorch = enableTorch) + Button(modifier = Modifier.fillMaxSize(), + onClick = { enableTorch = !enableTorch}, + colors = ButtonDefaults.buttonColors( + Color.Transparent), + shape = RoundedCornerShape(0.dp) + ) { + + } + AnimatedVisibility( + visible = scan, + enter = scaleIn( + animationSpec = tween(300), + initialScale = 0f, + transformOrigin = TransformOrigin.Center), + exit = scaleOut( + animationSpec = tween(300), + targetScale = 0f, + transformOrigin = TransformOrigin.Center), + ) { + Canvas( + modifier = Modifier.fillMaxSize() + ) { + drawCircle(color = R.color.components.requireColor(requireContext()), radius = 48f ,center = Offset(x.dp.toPx(),y.dp.toPx()) ) + drawCircle(color = R.color.surface.requireColor(requireContext()), radius = 40f ,center = Offset(x.dp.toPx(),y.dp.toPx()) ) + Log.d("initScale","$x/$y") + + this@QrScanningFragment.width = this.size.width + this@QrScanningFragment.height = this.size.height + } + } + Text(modifier = Modifier.padding(bottom = 32.dp), text = "轻触屏幕点亮灯光", color = Color.White) + Dialog(openDialog,result) + + + + + } + + + + } + + } + + @Composable + fun CameraView( + modifier: Modifier = Modifier, + preview: Preview, + imageCapture: ImageCapture? = null, + imageAnalysis: ImageAnalysis? = null, + cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, + scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER, + enableTorch: Boolean = false, + focusOnTap: Boolean = false + ) { + + val context = LocalContext.current + + //1 + val previewView = remember { PreviewView(context) } + val lifecycleOwner = LocalLifecycleOwner.current + + val cameraProvider by produceState(initialValue = null) { + value = context.getCameraProvider() + } + + val camera = remember(cameraProvider) { + cameraProvider?.let { + it.unbindAll() + it.bindToLifecycle( + lifecycleOwner, + cameraSelector, + *listOfNotNull(preview, imageAnalysis, imageCapture).toTypedArray() + ) + + } + } + + + // 2 + LaunchedEffect(true) { + preview.setSurfaceProvider(previewView.surfaceProvider) + previewView.scaleType = scaleType + } + + + LaunchedEffect(camera, enableTorch) { + // 控制闪光灯 + camera?.let { + if (it.cameraInfo.hasFlashUnit()) { + it.cameraControl.enableTorch(context, enableTorch) + } + } + } + + DisposableEffect(Unit) { + onDispose { + cameraProvider?.unbindAll() + } + } + AndroidView( + { previewView }, + modifier = modifier + .fillMaxSize() + .pointerInput(camera, focusOnTap) { + if (!focusOnTap) return@pointerInput + + detectTapGestures { + val meteringPointFactory = SurfaceOrientedMeteringPointFactory( + size.width.toFloat(), + size.height.toFloat() + ) + + // 点击屏幕聚焦 + val meteringAction = FocusMeteringAction + .Builder( + meteringPointFactory.createPoint(it.x, it.y), + FocusMeteringAction.FLAG_AF + ) + .disableAutoCancel() + .build() + + camera?.cameraControl?.startFocusAndMetering(meteringAction) + } + + + }, + ) + } + + + + private suspend fun Context.getCameraProvider(): ProcessCameraProvider = + suspendCoroutine { continuation -> + ProcessCameraProvider.getInstance(this).also { cameraProvider -> + cameraProvider.addListener({ + continuation.resume(cameraProvider.get()) + + + + + }, ContextCompat.getMainExecutor(this)) + } + } + + private suspend fun CameraControl.enableTorch(context: Context, torch: Boolean): Unit = + suspendCoroutine { + enableTorch(torch).addListener( + {}, + ContextCompat.getMainExecutor(context) + ) + } + private fun initScale(imageWidth : Int, imageHeight : Int){ + + scaleX = width / imageWidth.toFloat() + scaleY = height / imageHeight.toFloat() + //Log.d("initScale","$height/$imageHeight") + + + } + + @Composable + fun Dialog(openDialog: MutableState,result :String){ + if (openDialog.value) { + AlertDialog( + onDismissRequest = { + // Dismiss the dialog when the user clicks outside the dialog or on the back + // button. If you want to disable that functionality, simply use an empty + // onDismissRequest. + openDialog.value = false + }, + title = { + Text(text = "结果") + }, + text = { + Text(text = result) + }, + confirmButton = { + TextButton( + onClick = { + openDialog.value = false + } + ) { + Text("取消") + } + }, + dismissButton = { + TextButton( + onClick = { + openDialog.value = false + val intent = Intent(requireContext(),MainActivity::class.java) + + if ((getResources().getConfiguration().screenLayout and + Configuration.SCREENLAYOUT_SIZE_MASK) === + Configuration.SCREENLAYOUT_SIZE_LARGE) + { + createSession(result,requireActivity()) + }else { + intent.data = Uri.parse(result) + intent.flags=Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + } + } + ) { + Text("访问") + } + } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/AddonsAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/AddonsAdapter.kt new file mode 100644 index 0000000..a936254 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/AddonsAdapter.kt @@ -0,0 +1,56 @@ +package org.mozilla.xiu.browser.componets + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.CompoundButton +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.databinding.ItemAddonsManagerBinding +import kotlinx.coroutines.launch +import org.mozilla.geckoview.* + +class AddonsAdapter : ListAdapter(MenuAddonsListCallback) { + lateinit var select: Select + + inner class ItemTestViewHolder(private val binding: ItemAddonsManagerBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: WebExtension, mContext: Context){ + mContext as LifecycleOwner + var webExtensionController=GeckoRuntime.getDefault(mContext).webExtensionController + mContext.lifecycleScope.launch { + binding.textView8.text = bean.metaData.name + } + bean.metaData.icon.getBitmap(72).accept { binding.imageView3.setImageBitmap(it) } + + binding.materialCardView4.setOnClickListener { select.onSelect(bean) } + binding.switch1.isChecked=bean.metaData.enabled + binding.switch1.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { compoundButton, b -> + if (b) webExtensionController.enable( + bean, + WebExtensionController.EnableSource.USER + ) else webExtensionController.disable( + bean, + WebExtensionController.EnableSource.USER + ) + }) + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemAddonsManagerBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + interface Select{ + fun onSelect(bean: WebExtension) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/BookmarkDialog.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/BookmarkDialog.kt new file mode 100644 index 0000000..c7281cc --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/BookmarkDialog.kt @@ -0,0 +1,57 @@ +package org.mozilla.xiu.browser.componets + +import android.app.Activity +import android.content.DialogInterface +import android.view.LayoutInflater +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.database.bookmark.Bookmark +import org.mozilla.xiu.browser.database.bookmark.BookmarkViewModel +import org.mozilla.xiu.browser.database.shortcut.Shortcut +import org.mozilla.xiu.browser.database.shortcut.ShortcutViewModel +import org.mozilla.xiu.browser.databinding.DiaBookmarkBinding +import org.mozilla.xiu.browser.fxa.sync.BookmarkSync + + +class BookmarkDialog( + private var context1: Activity, + title: String?, + url: String? +) : + MyDialog(context1) { + var diaBookmarkBinding: DiaBookmarkBinding = DiaBookmarkBinding.inflate(LayoutInflater.from(context)) + var shortcutViewModel: ShortcutViewModel = ViewModelProvider(context1 as ViewModelStoreOwner).get(ShortcutViewModel::class.java) + var bookmarkViewModel: BookmarkViewModel = ViewModelProvider(context1 as ViewModelStoreOwner).get(BookmarkViewModel::class.java) + + init { + diaBookmarkBinding.textView5.setText(R.string.dia_add_bookmark_title) + diaBookmarkBinding.diaBookmarkTitle.setText(title) + diaBookmarkBinding.diaBookmarkUrl.setText(url) + setView(diaBookmarkBinding.getRoot()) + setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.confirm), + DialogInterface.OnClickListener { dialogInterface, i -> + val bookmark = Bookmark( + diaBookmarkBinding.diaBookmarkUrl.getText().toString(), + diaBookmarkBinding.diaBookmarkTitle.getText().toString(), + "默认", + diaBookmarkBinding.radioButton.isChecked + ) + val shortcut = Shortcut( + diaBookmarkBinding.diaBookmarkUrl.getText().toString(), + diaBookmarkBinding.diaBookmarkTitle.getText().toString(), + 0 + ) + if (diaBookmarkBinding.radioButton.isChecked) + shortcutViewModel.insertShortcuts(shortcut) + bookmarkViewModel.insertBookmarks(bookmark) + BookmarkSync(context1).sync(diaBookmarkBinding.diaBookmarkUrl.getText().toString(),diaBookmarkBinding.diaBookmarkTitle.getText().toString()) + }) + setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), + DialogInterface.OnClickListener { dialogInterface, i -> dialogInterface.dismiss() }) + } + + fun open() { + super.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/CollectionAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/CollectionAdapter.kt new file mode 100644 index 0000000..b93caba --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/CollectionAdapter.kt @@ -0,0 +1,24 @@ +package org.mozilla.xiu.browser.componets + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import org.mozilla.xiu.browser.MainActivity + +class CollectionAdapter(activity: MainActivity, private val fragmentlist:List) : FragmentStateAdapter(activity) {//fragment 也可以换为 activity +private val fid2 = 222L + private val fid3 = 333L + private val ids = arrayListOf(fid2,fid3) + private val creatID= hashSetOf() + override fun getItemCount(): Int { + return fragmentlist.size + } + + override fun createFragment(position: Int): Fragment { + val id = ids[position] + creatID.add(id) + val fragment = fragmentlist[position] + return fragment + }//返回需要创建的fragment + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/ContextMenuDialog.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/ContextMenuDialog.kt new file mode 100644 index 0000000..e1bd709 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/ContextMenuDialog.kt @@ -0,0 +1,96 @@ +package org.mozilla.xiu.browser.componets + +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.Glide +import com.google.android.material.button.MaterialButton +import com.kongzue.dialogx.dialogs.PopTip +import com.kongzue.dialogx.interfaces.OnBindView +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.databinding.DiaContextmenuBinding +import org.mozilla.xiu.browser.download.DownloadTask +import org.mozilla.xiu.browser.download.DownloadTaskLiveData +import org.mozilla.xiu.browser.utils.UriUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement + +class ContextMenuDialog(var context: FragmentActivity, element: ContextElement) : + AlertDialog(context) { + var binding: DiaContextmenuBinding + var type: String? = null + var downloadTasks=ArrayList() + + init { + binding = DiaContextmenuBinding.inflate(LayoutInflater.from(context)) + Glide.with(context).load(element.srcUri).into(binding.imageView19) + DownloadTaskLiveData.getInstance().observe(context){ + downloadTasks = it + } + binding.diaContextmenuDownloadButton.setOnClickListener(View.OnClickListener { + PopTip.build() + .setCustomView(object : OnBindView(org.mozilla.xiu.browser.R.layout.pop_mytip) { + override fun onBind(dialog: PopTip?, v: View) { + v.findViewById(R.id.textView17).text = "网页希望下载文件" + v.findViewById(R.id.materialButton7).setOnClickListener { + context.lifecycleScope.launch { + var downloadTask = element.srcUri?.let { it1 -> + DownloadTask(context, it1, + withContext(Dispatchers.IO) { + UriUtils.getFileName(it1) + }) + } + if (downloadTask != null) { + downloadTask.open() + downloadTasks.add(downloadTask) + DownloadTaskLiveData.getInstance().Value(downloadTasks) + } + } + } + } + }) + .show() + }) + binding.diaContextmenuCopyButton.setOnClickListener(View.OnClickListener { + copyToClipboard(context, element.srcUri) + dismiss() + }) + if (element.srcUri == null) binding.diaContextmenuOpenButton.setVisibility(View.GONE) + binding.diaContextmenuOpenButton.setOnClickListener(View.OnClickListener { + val intent = Intent() + intent.action = Intent.ACTION_VIEW + if (element.type == ContextElement.TYPE_IMAGE) type = + "image/*" else if (element.type == ContextElement.TYPE_VIDEO) type = "video/*" + val uri = Uri.parse(element.srcUri) + intent.setDataAndType(uri, type) + context.startActivity(intent) + }) + setView(binding.getRoot()) + } + + fun open() { + window!!.setBackgroundDrawable(context.getDrawable(R.drawable.bg_dialog)) + show() + } + + companion object { + fun copyToClipboard(context: Context, content: String?) { + // 从 API11 开始 android 推荐使用 android.content.ClipboardManager + // 为了兼容低版本我们这里使用旧版的 android.text.ClipboardManager,虽然提示 deprecated,但不影响使用。 + val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + // 将文本内容放到系统剪贴板里。 + cm.text = content + Toast.makeText(context, "已复制到剪切板", Toast.LENGTH_SHORT).show() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/HomeLivedata.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/HomeLivedata.kt new file mode 100644 index 0000000..27f2462 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/HomeLivedata.kt @@ -0,0 +1,23 @@ +package org.mozilla.xiu.browser.componets + +import androidx.lifecycle.LiveData + +class HomeLivedata: LiveData() { + fun Value(boolean: Boolean){ + postValue(boolean) + } + override fun onActive() { + super.onActive() + } + + override fun onInactive() { + super.onInactive() + } + companion object { + private lateinit var globalData: HomeLivedata + fun getInstance(): HomeLivedata { + globalData = if (Companion::globalData.isInitialized) globalData else HomeLivedata() + return globalData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddons.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddons.kt new file mode 100644 index 0000000..5cd4fa2 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddons.kt @@ -0,0 +1,11 @@ +package org.mozilla.xiu.browser.componets + +import android.content.Context +import org.mozilla.xiu.browser.databinding.PopupMenuBinding + +class MenuAddons { + fun add(binding: PopupMenuBinding,context: Context){ + + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddonsAdapater.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddonsAdapater.kt new file mode 100644 index 0000000..17b774e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddonsAdapater.kt @@ -0,0 +1,77 @@ +package org.mozilla.xiu.browser.componets + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.componets.popup.AddonsPopup +import org.mozilla.xiu.browser.databinding.ItemMenuAddonsBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.WebExtension + +class MenuAddonsAdapater : ListAdapter(MenuAddonsListCallback) { + lateinit var select: Select + + inner class ItemTestViewHolder(private val binding: ItemMenuAddonsBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: WebExtension, mContext: Context){ + mContext as LifecycleOwner + val addonsPopup= AddonsPopup(mContext) + bean.setActionDelegate(object :WebExtension.ActionDelegate{ + override fun onBrowserAction( + extension: WebExtension, + session: GeckoSession?, + action: WebExtension.Action + ) { + mContext.lifecycleScope.launch { + binding.addonsIcon.setImageBitmap(withContext(Dispatchers.IO) { + action.icon?.getBitmap( + 72 + )?.poll() + }) + } + binding.addonsIcon.setOnClickListener { action.click() } + } + override fun onTogglePopup( + extension: WebExtension, + action: WebExtension.Action + ): GeckoResult? { + val session=GeckoSession() + addonsPopup.show(session,extension) + return GeckoResult.fromValue(session) + } + override fun onOpenPopup( + extension: WebExtension, + action: WebExtension.Action + ): GeckoResult? { + val session=GeckoSession() + addonsPopup.show(session,extension) + return GeckoResult.fromValue(session) + } + }) + + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemMenuAddonsBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + interface Select{ + fun onSelect() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddonsListCallback.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddonsListCallback.kt new file mode 100644 index 0000000..dccb6d1 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/MenuAddonsListCallback.kt @@ -0,0 +1,22 @@ +package org.mozilla.xiu.browser.componets + +import android.annotation.SuppressLint +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.recyclerview.widget.DiffUtil +import mozilla.components.concept.storage.BookmarkNode +import org.mozilla.geckoview.WebExtension + +object MenuAddonsListCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: WebExtension, newItem: WebExtension): Boolean { + return oldItem == newItem + + + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: WebExtension, newItem: WebExtension): Boolean { + return oldItem == newItem + + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/MyBottomSheetDialog.java b/app/src/main/java/org/mozilla/xiu/browser/componets/MyBottomSheetDialog.java new file mode 100644 index 0000000..c0bb737 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/MyBottomSheetDialog.java @@ -0,0 +1,44 @@ +package org.mozilla.xiu.browser.componets; + +import android.content.Context; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; + +public class MyBottomSheetDialog extends BottomSheetDialog { + private BottomSheetBehavior behavior; + int mode; + public MyBottomSheetDialog(@NonNull Context context, int theme, int mode) { + super(context, theme); + this.mode=mode; + } + + + @Override + protected void onStart() { + super.onStart(); + switch (mode){ + case 0: + if (behavior != null && behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { + behavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + break; + case 1: + BottomSheetBehavior behavior = getBehavior(); + behavior.setState(BottomSheetBehavior.STATE_HALF_EXPANDED); + break; + case 2: + behavior = getBehavior(); + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + break; + } + + + + } + + } + diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/MyDialog.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/MyDialog.kt new file mode 100644 index 0000000..491341c --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/MyDialog.kt @@ -0,0 +1,12 @@ +package org.mozilla.xiu.browser.componets + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import org.mozilla.xiu.browser.R + + +open class MyDialog(context: Context) : AlertDialog(context) { + init { + window!!.setBackgroundDrawable(context.getDrawable(R.drawable.bg_dialog)) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/PermissionDialog.java b/app/src/main/java/org/mozilla/xiu/browser/componets/PermissionDialog.java new file mode 100644 index 0000000..e29a753 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/PermissionDialog.java @@ -0,0 +1,112 @@ +package org.mozilla.xiu.browser.componets; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.LayoutInflater; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import org.mozilla.xiu.browser.R; +import org.mozilla.xiu.browser.databinding.DiaInstallBinding; + +import org.mozilla.geckoview.WebExtension; + +import java.util.Arrays; + +public class PermissionDialog extends MaterialAlertDialogBuilder +{ + int dialogResult; + Handler mHandler ; + DiaInstallBinding binding; + Activity activity; + + + public PermissionDialog(Activity context, WebExtension webExtension) + { + + super(context); + onCreate(webExtension); + this.activity=context; + + + } + public int getDialogResult() + { + return dialogResult; + } + public void setDialogResult(int dialogResult) + { + this.dialogResult = dialogResult; + } + /** Called when the activity is first created. */ + + public void onCreate(WebExtension webExtension) { + binding=DiaInstallBinding.inflate(LayoutInflater.from(getContext())); + binding.textView23.setText("要添加"+webExtension.metaData.name+"吗?"); + binding.textView22.setText("需要以下权限:"); + setIcon(R.drawable.extension_puzzle); + binding.diaPer.setText(Arrays.toString(webExtension.metaData.permissions) + .replaceAll("\\[", "• ") + .replaceAll(",","\n•") + .replaceAll("\\]","") + .replaceAll(getContext().getString(R.string.per_tabs), getContext().getString(R.string.per_tabs_cn)) + .replaceAll(getContext().getString(R.string.per_bookmarks), getContext().getString(R.string.per_bookmarks_cn)) + .replaceAll(getContext().getString(R.string.per_clipboardRead), getContext().getString(R.string.per_clipboardRead_cn)) + .replaceAll(getContext().getString(R.string.per_browserSettings), getContext().getString(R.string.per_browserSettings_cn)) + .replaceAll(getContext().getString(R.string.per_browsingData), getContext().getString(R.string.per_browsingData_cn)) + .replaceAll(getContext().getString(R.string.per_downloads), getContext().getString(R.string.per_downloads_cn)) + .replaceAll(getContext().getString(R.string.per_geolocation), getContext().getString(R.string.per_geolocation_cn)) + .replaceAll(getContext().getString(R.string.per_notifications), getContext().getString(R.string.per_notifications_cn)) + ); + + + + setView(binding.getRoot()); + setNegativeButton("取消", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + endDialog(0); + } + }); + setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + endDialog(1); + } + }); + + + } + + public void endDialog(int result) + { + setDialogResult(result); + Message m = mHandler.obtainMessage(); + mHandler.sendMessage(m); + } + + @SuppressLint("HandlerLeak") + public int showDialog() + { + mHandler = new Handler() { + @Override + public void handleMessage(Message mesg) { + // process incoming messages here + //super.handleMessage(msg); + throw new RuntimeException(); + } + }; + super.show().getWindow().setBackgroundDrawable(activity.getDrawable(R.drawable.bg_dialog)); + try { + Looper.getMainLooper().loop(); + } + catch(RuntimeException e2) + { + } + return dialogResult; + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/StageFxAEntryPoint.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/StageFxAEntryPoint.kt new file mode 100644 index 0000000..65d3704 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/StageFxAEntryPoint.kt @@ -0,0 +1,115 @@ +package org.mozilla.xiu.browser.componets + +import android.os.Parcel +import android.os.Parcelable +import mozilla.components.concept.sync.FxAEntryPoint + +/** + * Fenix implementation of [FxAEntryPoint]. + */ +enum class StageFxAEntryPoint (override val entryName: String) : FxAEntryPoint, Parcelable { + /** + * New user onboarding, the user accessed the sign in through new user onboarding + */ + NewUserOnboarding("newuser-onboarding"), + + /** + * Manual sign in from the onboarding menu + */ + OnboardingManualSignIn("onboarding-manual-sign-in"), + + /** + * User used a deep link to get to firefox accounts authentication + */ + DeepLink("deep-link"), + + /** + * Authenticating from the browser's toolbar + */ + BrowserToolbar("browser-toolbar"), + + /** + * Authenticating from the home menu (the hamburger menu) + */ + HomeMenu("home-menu"), + + /** + * Authenticating in the bookmark view, when getting attempting to get synced + * bookmarks + */ + BookmarkView("bookmark-view"), + + /** + * Authenticating from the homepage onboarding dialog + */ + HomeOnboardingDialog("home-onboarding-dialog"), + + /** + * Authenticating from the settings menu + */ + SettingsMenu("settings-menu"), + + /** + * Authenticating from the autofill settings to enable synced + * credit cards/addresses + */ + AutofillSetting("autofill-setting"), + + /** + * Authenticating from the saved logins menu to enable synced + * logins + */ + SavedLogins("saved-logins"), + + /** + * Authenticating from the Share menu to enable send tab + */ + ShareMenu("share-menu"), + + /** + * Authenticating as a navigation interaction + */ + NavigationInteraction("navigation-interaction"), + + /** + * Authenticating from the synced tabs menu to enable synced tabs + */ + SyncedTabsMenu("synced-tabs-menu"), + + /** + * When serializing the value after navigating, the result is a nullable value. We have this + * "unknown" as a default value in the odd chance that we receive an [entryName] is not part of this enum. + * + * Do not use within app code. + */ + Unknown("unknown"), + ; + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(entryName) + } + + override fun describeContents(): Int { + return 0 + } + + /** + * Override implementation of the [Parcelable.Creator]. + * + * Implementation notes: We need to manually create an override for [Parcelable] instead of using the annotation, + * because this is an enum implementation of the API and the auto-generated code does not know how to choose a + * particular enum value in [Parcelable.Creator.createFromParcel]. + * We also introduce an [FxAEntryPoint.Unknown] value to use as a default return value in the off-chance that we + * cannot safely serialize the enum value from the navigation library; this should be a rare case, if any. + */ + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): StageFxAEntryPoint { + val parcelEntryName = parcel.readString() ?: Unknown + return StageFxAEntryPoint.values().first { it.entryName == parcelEntryName } + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/TabBottomSheetDialog.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/TabBottomSheetDialog.kt new file mode 100644 index 0000000..0cd482b --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/TabBottomSheetDialog.kt @@ -0,0 +1,26 @@ +package org.mozilla.xiu.browser.componets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.mozilla.xiu.browser.R + +class TabBottomSheetDialog : BottomSheetDialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.popup_tab, container, false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, com.google.android.material.R.style.Base_ThemeOverlay_Material3_BottomSheetDialog) + } + + + companion object { + const val TAG = "ModalBottomSheet" + }} diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/binding/BindingUtils.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/binding/BindingUtils.kt new file mode 100644 index 0000000..90d8851 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/binding/BindingUtils.kt @@ -0,0 +1,92 @@ +package org.mozilla.xiu.browser.componets.binding + +import android.graphics.Bitmap +import android.os.Build +import android.view.View +import android.widget.ImageView +import android.widget.ProgressBar +import androidx.annotation.RequiresApi +import androidx.databinding.BindingAdapter +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.request.RequestOptions +import com.google.android.material.button.MaterialButton +import com.google.android.material.card.MaterialCardView +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.utils.RoundedCornersTransform +import org.mozilla.xiu.browser.utils.Utils.dip2px +import org.mozilla.geckoview.GeckoView +import java.net.URI + +@BindingAdapter(value = ["imageBitmap"], requireAll = false) + fun loadImage(view: ImageView, bitmap: Bitmap) { + if (bitmap == null) return + //默认裁剪四个圆角,不需要设置圆角,对应参数设为false + Glide.with(view.context) + .load(bitmap) + .apply( + RequestOptions().transform( + CenterCrop(), RoundedCornersTransform( + view.context, 16f + ) + ) + ) + .into(view) + } + +@RequiresApi(Build.VERSION_CODES.M) +@BindingAdapter(value = ["active"], requireAll = false) +fun isActive(view: MaterialCardView, boolean: Boolean) { + if (boolean == null) return + if (boolean) + view.visibility= View.VISIBLE + else view.visibility= View.GONE +} +@RequiresApi(Build.VERSION_CODES.M) +@BindingAdapter(value = ["activeL"], requireAll = false) +fun isActiveL(view: ProgressBar, boolean: Boolean) { + if (boolean == null) return + if (boolean) + view.visibility= View.VISIBLE + else view.visibility= View.GONE + +} + +@BindingAdapter(value = ["iconUri"], requireAll = false) +fun loadIcon(view: ImageView, url: String?) { + if (url == null) return + val uri = URI.create(url) + val faviconUrl = uri.scheme + "://" + uri.host + "/favicon.ico" + Glide.with(view.context).load(faviconUrl).placeholder(R.drawable.globe) + .into(view) +} +@BindingAdapter(value = ["stateIcon"], requireAll = false) +fun stateIcon(view: MaterialButton, state: Int?) { + if (state == null) return + when(state){ + 0->view.icon=view.context.getDrawable(R.drawable.play_circle) + 1->view.icon=view.context.getDrawable(R.drawable.pause_circle) + 2->view.icon=view.context.getDrawable(R.drawable.play_circle) + + } + +} +@BindingAdapter(value = ["secureIcon"], requireAll = false) +fun secureIcon(view: MaterialButton, isSecure: Boolean?) { + if (isSecure == null) return + if (isSecure) view.icon=view.context.getDrawable(R.drawable.shield_fill) + else view.icon=view.context.getDrawable(R.drawable.shield_slash_fill) +} + +@BindingAdapter(value = ["secureText"], requireAll = false) +fun secureButtonText(view: MaterialButton, isSecure: Boolean?) { + if (isSecure == null) return + if (isSecure) view.text=view.context.getString(R.string.connection_secure) + else view.text=view.context.getText(R.string.connection_not_secure) +} + +@BindingAdapter(value = ["dynamicToolbar"], requireAll = false) +fun dynamicToolbar(view: GeckoView,int:Int) { + view.setDynamicToolbarMaxHeight(int+dip2px(view.context,64)) +} + diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/AccountPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/AccountPopup.kt new file mode 100644 index 0000000..0ea07c1 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/AccountPopup.kt @@ -0,0 +1,266 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.blur +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.componets.TabBottomSheetDialog.Companion.TAG +import org.mozilla.xiu.browser.fxa.AccountManagerCollection +import org.mozilla.xiu.browser.fxa.AccountProfile +import org.mozilla.xiu.browser.fxa.AccountProfileViewModel +import org.mozilla.xiu.browser.fxa.AccountStateViewModel +import org.mozilla.xiu.browser.utils.Utils.requireColor +import kotlinx.coroutines.launch +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sync.SyncReason + +class AccountPopup : BottomSheetDialogFragment() { + private lateinit var fxaViewModel :AccountProfileViewModel + private lateinit var accountManagerCollection: AccountManagerCollection + private lateinit var accountStateViewModel: AccountStateViewModel + private lateinit var accountManager: FxaAccountManager + + + + @OptIn(ExperimentalGlideComposeApi::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + fxaViewModel = ViewModelProvider(requireActivity())[AccountProfileViewModel::class.java] + accountManagerCollection = ViewModelProvider(requireActivity())[AccountManagerCollection::class.java] + accountStateViewModel = ViewModelProvider(requireActivity())[AccountStateViewModel::class.java] + lifecycleScope.launch { + accountManagerCollection.data.collect(){ + accountManager = it + + } + } + + setContent { + MaterialTheme { + val profile by fxaViewModel.data.collectAsState() + val panelOption = remember { + arrayListOf("立即同步","接受标签","退出账户") + } + ConstraintLayout(modifier = Modifier.fillMaxSize()) { + val (avatar,panel,background,lottie) = createRefs() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + GlideImage( + model = profile.avatar, + contentDescription = "", + modifier = Modifier + .width(1024.dp) + .height(1024.dp) + .constrainAs(background) { + width = Dimension.fillToConstraints + height = Dimension.fillToConstraints + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + } + .blur(256.dp) + ) + } + + avatar(modifier = Modifier.constrainAs(avatar){ + top.linkTo(parent.top) + start.linkTo(parent.start) + //bottom.linkTo(parent.bottom) + } ,profile) + operationPanel(modifier = Modifier + .height(256.dp) + .constrainAs(panel) { + width = Dimension.fillToConstraints + top.linkTo(avatar.bottom, 32.dp) + start.linkTo(parent.start, 16.dp) + end.linkTo(parent.end, 16.dp) + //bottom.linkTo(parent.bottom,16.dp) + },panelOption) + + } + + + + + } + } + } + @OptIn(ExperimentalGlideComposeApi::class) + @Composable + fun avatar(modifier: Modifier,profile: AccountProfile){ + + + ConstraintLayout(modifier = modifier) { + val (avatar,name,email) = createRefs() + GlideImage( + model = profile.avatar, + contentDescription = "", + modifier = Modifier + .width(72.dp) + .height(72.dp) + .constrainAs(avatar) { + top.linkTo(parent.top, 32.dp) + start.linkTo(parent.start, 16.dp) + + } + ){ + it.circleCrop() + } + + + Text(text =profile.displayName.toString(), + modifier = Modifier.constrainAs(name){ + top.linkTo(avatar.top) + start.linkTo(avatar.end, 16.dp) + } + , + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = R.color.components.requireColor(requireContext()) + ) + Text(text =profile.email.toString(), + modifier = Modifier.constrainAs(email){ + start.linkTo(avatar.end, 16.dp) + bottom.linkTo(avatar.bottom) + } + , fontSize = 14.sp, + color = R.color.components.requireColor(requireContext()) + ) + + + + + } + + } + @Composable + fun operationPanel(modifier: Modifier,panelOption:ArrayList){ + ConstraintLayout(modifier = modifier) { + var (background,option) = createRefs() + Canvas( modifier = Modifier + .alpha(0.5f) + .fillMaxSize() + .constrainAs(background) + { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, onDraw = { + val canvasWidth = size.width + val canvasHeight = size.height + drawRoundRect(Color(requireActivity().getColor(R.color.onSurface)), size = Size(width = canvasWidth, height = canvasHeight),cornerRadius = CornerRadius(64F,64F)) + }) + + LazyColumn(content = { + items (panelOption.size) + { + Column(modifier = Modifier + .fillMaxSize() + .clickable { + when (it) { + 0 -> { + lifecycleScope.launch { + accountManager.syncNow(SyncReason.User) + accountManager + .authenticatedAccount() + ?.deviceConstellation() + ?.pollForCommands() + + } + + } + + 1 -> { + ReceivedTabPopup().show(parentFragmentManager,TAG) + dismiss() + } + 2 -> { + lifecycleScope.launch { + accountManager.logout() + } + dismiss() + } + } + } + .height(64.dp), + verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = panelOption[it],modifier = Modifier, color = R.color.components.requireColor(requireContext())) + // Divider() + } + } + + }, + modifier = Modifier + .constrainAs(option) + { + width = Dimension.fillToConstraints + top.linkTo(background.top) + bottom.linkTo(background.bottom) + start.linkTo(background.start) + end.linkTo(background.end) + } + ) + + + + + + + } + } + @Composable + fun operationItem(modifier: Modifier,text:String){ + Button(onClick = { /*TODO*/ }, modifier = modifier + .padding(16.dp) + .height(72.dp), shape = ButtonDefaults.textShape) { + Text(text = text) + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/AddonsPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/AddonsPopup.kt new file mode 100644 index 0000000..087310e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/AddonsPopup.kt @@ -0,0 +1,70 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetDialog +import kotlinx.coroutines.launch +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.WebExtension +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.databinding.PopupAddonsBinding + +class AddonsPopup { + val context: Context + lateinit var bottomSheetDialog: BottomSheetDialog + lateinit var binding: PopupAddonsBinding + constructor( + context: Context, + ) { + this.context = context + bottomSheetDialog = BottomSheetDialog(context, R.style.BottomSheetDialog ) + binding = PopupAddonsBinding.inflate(LayoutInflater.from(context)) + bottomSheetDialog.setContentView(binding.root) + bottomSheetDialog.behavior.isDraggable=false + binding.button.setOnClickListener { bottomSheetDialog.dismiss()} + + + } + fun show(session:GeckoSession, extension: WebExtension){ + session.open(GeckoRuntime.getDefault(context)) + session.promptDelegate = object : GeckoSession.PromptDelegate { + override fun onChoicePrompt( + session: GeckoSession, + prompt: GeckoSession.PromptDelegate.ChoicePrompt + ): GeckoResult? { + //prompt. + val jsChoiceDialog = + org.mozilla.xiu.browser.broswer.dialog.JsChoiceDialog( + context, + prompt + ) + jsChoiceDialog.showDialog() + return GeckoResult.fromValue(prompt.confirm(jsChoiceDialog.dialogResult.toString())) + } + + override fun onAlertPrompt( + session: GeckoSession, + prompt: GeckoSession.PromptDelegate.AlertPrompt + ): GeckoResult? { + val alertDialog = + org.mozilla.xiu.browser.broswer.dialog.AlertDialog(context, prompt) + alertDialog.showDialog() + return GeckoResult.fromValue(alertDialog.getDialogResult()) + } + } + context as LifecycleOwner + context.lifecycleScope.launch { + extension.metaData.icon.getBitmap(72).accept { binding.imageView5.setImageBitmap(it) } + + binding.textView.text=extension.metaData.name + } + binding.addonsView.coverUntilFirstPaint(Color.WHITE) + binding.addonsView.setSession(session) + bottomSheetDialog.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/BookmarkPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/BookmarkPopup.kt new file mode 100644 index 0000000..f670374 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/BookmarkPopup.kt @@ -0,0 +1,42 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.view.LayoutInflater +import androidx.fragment.app.Fragment +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.tabs.TabLayoutMediator +import org.mozilla.xiu.browser.* +import org.mozilla.xiu.browser.componets.CollectionAdapter +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.broswer.bookmark.BookmarkFragment +import org.mozilla.xiu.browser.broswer.bookmark.sync.SyncBookmarkFolderFragment +import org.mozilla.xiu.browser.databinding.PopupBookmarkBinding + +class BookmarkPopup { + val context: MainActivity + lateinit var bottomSheetDialog: BottomSheetDialog + lateinit var binding: PopupBookmarkBinding + + private val fragments=listOf(BookmarkFragment(), SyncBookmarkFolderFragment()) + constructor( + context: MainActivity + ) { + this.context = context + bottomSheetDialog = BottomSheetDialog(context, R.style.BottomSheetDialog ) + binding = PopupBookmarkBinding.inflate(LayoutInflater.from(context)) + bottomSheetDialog.setContentView(binding.root) + binding.bookmarkViewPager.adapter= CollectionAdapter(context,fragments) + TabLayoutMediator(binding.tabLayout,binding.bookmarkViewPager){ tab,position-> + when (position) { + 0 -> tab.setIcon(R.drawable.bookmarks) + 1 -> tab.setIcon(R.drawable.sync_circle) + } + }.attach() + HomeLivedata.getInstance().observe(context){ + if (!it) bottomSheetDialog.dismiss() + } + + } + fun show(){ + bottomSheetDialog.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ComposeMenuPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ComposeMenuPopup.kt new file mode 100644 index 0000000..b8de99d --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ComposeMenuPopup.kt @@ -0,0 +1,297 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.repeatable +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.fxa.AccountManagerCollection +import org.mozilla.xiu.browser.fxa.SyncDevicesObserver +import org.mozilla.xiu.browser.session.DelegateLivedata +import org.mozilla.xiu.browser.session.SessionDelegate +import kotlinx.coroutines.launch +import mozilla.components.concept.sync.Device +import mozilla.components.concept.sync.DeviceCommandOutgoing +import mozilla.components.concept.sync.DeviceType +import mozilla.components.service.fxa.manager.FxaAccountManager + +class ComposeMenuPopup :BottomSheetDialogFragment(){ + private lateinit var syncDevicesObserver: SyncDevicesObserver + private lateinit var accountManagerCollection: AccountManagerCollection + private lateinit var accountManager: FxaAccountManager + private val sendDevices = arrayListOf() + private lateinit var sessionDelegate: SessionDelegate + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + syncDevicesObserver = ViewModelProvider(requireActivity())[SyncDevicesObserver::class.java] + accountManagerCollection = ViewModelProvider(requireActivity())[AccountManagerCollection::class.java] + lifecycleScope.launch { + accountManagerCollection.data.collect(){ + accountManager = it + + } + } + + + } + @OptIn(ExperimentalAnimationApi::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + DelegateLivedata.getInstance().observe(viewLifecycleOwner){ + sessionDelegate = it + } + setContent { + MyTheme() { + val deviceList = syncDevicesObserver.syncDevicesStateFlow.collectAsState() + var moveToRight by remember { mutableStateOf(false) } + val targetValue = if(moveToRight) 200.dp else 0.dp + val animation1 = tween(durationMillis = 500) + val startPadding1 by animateDpAsState(targetValue, animationSpec = repeatable(1, animation1), + label = "" + ) + ConstraintLayout { + + val (devices,pushButton,tip,title,url) = createRefs() + Text( + text = "向其他设备推送标签", + modifier = Modifier.constrainAs(tip) { + top.linkTo(parent.top,8.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + fontSize = 16.sp + + ) + FilledIconButton( + onClick ={ + moveToRight = true + lifecycleScope.launch { + accountManager.authenticatedAccount()?.deviceConstellation()?.let {constellation -> + sendDevices?.forEach { + moveToRight = !constellation.sendCommandToDevice( + it.id, + DeviceCommandOutgoing.SendTab(sessionDelegate.mTitle, sessionDelegate.u), + ) + sendDevices.remove(it) + + } + + } + } + + }, + modifier = Modifier + .size(128.dp) + .constrainAs(pushButton) { + top.linkTo(tip.bottom, 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { + Crossfade(!moveToRight, animationSpec = tween(500), label = "") { + // 使用状态进行判断 + if(it){ + Icon(painter = painterResource(id = R.drawable.send_fill), contentDescription ="send", modifier = Modifier.size(64.dp).padding(start = startPadding1,bottom =startPadding1 )) + + }else{ + Icon(painter = painterResource(id = R.drawable.check_circle_fill), contentDescription ="send", modifier = Modifier.size(64.dp)) + + } + } + } + + LazyVerticalGrid( + columns = GridCells.Fixed(4), + modifier = Modifier.constrainAs(devices){ + width = Dimension.wrapContent + height = Dimension.wrapContent + top.linkTo(pushButton.bottom,16.dp) + start.linkTo(parent.start,8.dp) + end.linkTo(parent.end,8.dp) + bottom.linkTo(parent.bottom,16.dp) + }, + content = { + items(deviceList.value.size){ + device(deviceList.value[it]) + } + }) + } + + + + + } + } + + + } + @ExperimentalAnimationApi + @Composable + fun device(device: Device) { + var shown by remember { mutableStateOf(false) } + + ConstraintLayout(modifier = Modifier.padding(8.dp)){ + val (myDevice,checked,name) = createRefs() + avatar(modifier = Modifier + .size(56.dp) + .clip(CircleShape) + .constrainAs(myDevice) { + start.linkTo(parent.start, 8.dp) + end.linkTo(parent.end, 8.dp) + top.linkTo(parent.top, 4.dp) + } + .clickable { + shown = !shown + if (shown) + sendDevices.add(device) + else + sendDevices.remove(device) + }, + int = getDeviceTypeAvatar(device.deviceType) + ) + AnimatedVisibility( + visible = shown, + // 进入动画设置 fadeIn ,动画规格设置 tween 时长 1000ms,初始透明度 0.3f + enter = scaleIn( + animationSpec = tween(300), + initialScale = 0f, + transformOrigin = TransformOrigin.Center), + exit = scaleOut( + animationSpec = tween(300), + targetScale = 0f, + transformOrigin = TransformOrigin.Center), + modifier = Modifier.constrainAs(checked){ + end.linkTo(myDevice.end) + bottom.linkTo(myDevice.bottom) + } + + ) { + checked() + } + + Text( + text = device.displayName, + modifier = Modifier.constrainAs(name){ + top.linkTo(myDevice.bottom,4.dp) + start.linkTo(parent.start,8.dp) + end.linkTo(parent.end,8.dp) + bottom.linkTo(parent.bottom,4.dp) + }, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + + ) + + } + } + @Composable + fun avatar(modifier: Modifier,int: Int){ + Box(modifier = modifier.size(64.dp),contentAlignment = Alignment.Center) { + Canvas(modifier = Modifier.size(64.dp)) { + drawCircle(requireColor(R.color.onSurface)) + + } + Image(painter = painterResource(id = int), contentDescription = "", modifier = Modifier.size(28.dp)) + + } + } + + @Composable + fun checked (){ + Box(modifier = Modifier.size(20.dp),contentAlignment = Alignment.Center) { + Canvas(modifier = Modifier.size(20.dp)) { + drawCircle(Color.White) + } + Image(painter = painterResource(id = R.drawable.check_circle_fill), contentDescription = "", modifier = Modifier.size(16.dp)) + + } + } + + + private fun requireColor(int: Int) :Color = Color(requireContext().getColor(int)) + private fun getDeviceTypeAvatar(deviceType: DeviceType):Int{ + DeviceType.MOBILE + return when(deviceType){ + DeviceType.MOBILE ->R.drawable.phone_fill + DeviceType.DESKTOP ->R.drawable.pc_display + + else -> R.drawable.phone_fill + } + + } + @Composable + fun MyTheme ( + dark: Boolean = isSystemInDarkTheme (), + dynamic: Boolean = Build. VERSION.SDK_INT >= Build.VERSION_CODES.S, + content: @Composable () -> Unit + ) { + // ColorScheme 配置以及 MaterialTheme + val colorScheme = if (dynamic) { + val context = LocalContext.current + if (dark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } else { + if (dark) dynamicDarkColorScheme(requireContext()) else dynamicLightColorScheme(requireContext()) + } + MaterialTheme( + colorScheme = colorScheme, + content = content, + ) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/HistoryPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/HistoryPopup.kt new file mode 100644 index 0000000..3a8a3d2 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/HistoryPopup.kt @@ -0,0 +1,41 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.view.LayoutInflater +import androidx.fragment.app.Fragment +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.tabs.TabLayoutMediator +import org.mozilla.xiu.browser.MainActivity +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.componets.CollectionAdapter +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.broswer.history.HistoryFragment +import org.mozilla.xiu.browser.databinding.PopupHistoryBinding + +class HistoryPopup { + val context: MainActivity + lateinit var bottomSheetDialog: BottomSheetDialog + lateinit var binding: PopupHistoryBinding + + private val fragments=listOf(HistoryFragment()) + constructor( + context: MainActivity, + ) { + this.context = context + bottomSheetDialog = BottomSheetDialog(context, R.style.BottomSheetDialog ) + binding = PopupHistoryBinding.inflate(LayoutInflater.from(context)) + bottomSheetDialog.setContentView(binding.root) + binding.historyViewPager.adapter= CollectionAdapter(context,fragments) + TabLayoutMediator(binding.historytabLayout,binding.historyViewPager){ tab,position-> + when (position) { + 0 -> tab.setIcon(R.drawable.hourglass_split) + } + }.attach() + HomeLivedata.getInstance().observe(context){ + if (!it) bottomSheetDialog.dismiss() + } + + } + fun show(){ + bottomSheetDialog.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/IntentPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/IntentPopup.kt new file mode 100644 index 0000000..dbfbd80 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/IntentPopup.kt @@ -0,0 +1,34 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.content.Context +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import com.google.android.material.bottomsheet.BottomSheetDialog +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.databinding.PopupIntentBinding + +class IntentPopup { + var context: Context? = null + var binding: PopupIntentBinding? = null + lateinit var bottomSheetDialog: BottomSheetDialog + + constructor(context: Context?) { + this.context = context + binding = PopupIntentBinding.inflate(LayoutInflater.from(context)) + bottomSheetDialog = BottomSheetDialog(context!!, R.style.BottomSheetDialog) + bottomSheetDialog.setContentView(binding!!.root) + } + + + fun show(intent: Intent?) { + + bottomSheetDialog.show() + binding!!.IntentButton.setOnClickListener(View.OnClickListener { + context!!.startActivity(intent) + bottomSheetDialog.dismiss() + }) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/MenuPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/MenuPopup.kt new file mode 100644 index 0000000..75bdcaa --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/MenuPopup.kt @@ -0,0 +1,194 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.app.Dialog +import android.content.Intent +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialog +import kotlinx.coroutines.launch +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSessionSettings +import org.mozilla.geckoview.StorageController.ClearFlags.ALL +import org.mozilla.xiu.browser.HolderActivity +import org.mozilla.xiu.browser.MainActivity +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.componets.BookmarkDialog +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.componets.MenuAddonsAdapater +import org.mozilla.xiu.browser.componets.TabBottomSheetDialog.Companion.TAG +import org.mozilla.xiu.browser.database.bookmark.BookmarkViewModel +import org.mozilla.xiu.browser.databinding.PopupMenuBinding +import org.mozilla.xiu.browser.session.DelegateLivedata +import org.mozilla.xiu.browser.session.PrivacyFlow +import org.mozilla.xiu.browser.session.SessionDelegate + + +class MenuPopup { + val context: MainActivity + private var bottomSheetDialog: BottomSheetDialog + var binding: PopupMenuBinding + private var bookmarkViewModel: BookmarkViewModel + private var sessionDelegate: SessionDelegate? = null + var isHome: Boolean = true + var isPrivacy: Boolean = false + + constructor( + context: MainActivity, + ) { + this.context = context + bottomSheetDialog = BottomSheetDialog(context, R.style.BottomSheetDialog) + bookmarkViewModel = + ViewModelProvider(context).get(BookmarkViewModel::class.java) + binding = PopupMenuBinding.inflate(LayoutInflater.from(context)) + bottomSheetDialog.setContentView(binding.root) + var privacyRename = ViewModelProvider(context)[PrivacyFlow::class.java] + + DelegateLivedata.getInstance().observe(context) { + sessionDelegate = it + binding.user = sessionDelegate + + } + context.lifecycleScope.launch { + privacyRename.data.collect() { value: Boolean -> + isPrivacy = value + if (value) + binding.privacyButton.icon = context.getDrawable(R.drawable.icon_privacy_fill) + else + binding.privacyButton.icon = context.getDrawable(R.drawable.icon_privacy) + } + } + + + + HomeLivedata.getInstance().observe(context) { + isHome = it + if (it) { + binding.constraintLayout5.visibility = View.GONE + } else { + binding.constraintLayout5.visibility = View.VISIBLE + } + } + binding.downloadButton.setOnClickListener { + var intent = Intent(context, HolderActivity::class.java) + intent.putExtra("Page", "DOWNLOAD") + context.startActivity(intent) + bottomSheetDialog.dismiss() + } + binding.settingButton.setOnClickListener { + var intent = Intent(context, HolderActivity::class.java) + intent.putExtra("Page", "SETTINGS") + context.startActivity(intent) + bottomSheetDialog.dismiss() + + } + binding.privacyButton.setOnClickListener { + if (isPrivacy) + privacyRename.changeMode(false) + else + privacyRename.changeMode(true) + bottomSheetDialog.dismiss() + } + binding.bookmarkButton.setOnClickListener { + BookmarkPopup(context).show() + bottomSheetDialog.dismiss() + + } + binding.historyButton.setOnClickListener { + HistoryPopup(context).show() + bottomSheetDialog.dismiss() + + } + binding.starButton.setOnClickListener { + if (sessionDelegate != null) { + BookmarkDialog(context, sessionDelegate!!.mTitle, sessionDelegate!!.u).show() + } + } + + binding.shareButton.setOnClickListener { + ComposeMenuPopup().show(context.supportFragmentManager, TAG) + bottomSheetDialog.dismiss() + } + + binding.reloadBotton.setOnClickListener { + if (!isHome) + sessionDelegate?.session?.reload() + bottomSheetDialog.dismiss() + + } + binding.forwardButton.setOnClickListener { + if (!isHome) + sessionDelegate?.session?.goForward() + bottomSheetDialog.dismiss() + + } + binding.dataClearingButton.setOnClickListener { + sessionDelegate?.let { it1 -> + GeckoRuntime.getDefault(context).storageController.clearDataFromHost( + it1.secureHost, ALL + ) + it1.session.reload() + + } + bottomSheetDialog.dismiss() + } + binding.modeBotton.setOnClickListener { + if (!isHome) { + if (sessionDelegate?.session?.settings?.userAgentMode == GeckoSessionSettings.USER_AGENT_MODE_DESKTOP) { + sessionDelegate!!.session.settings.userAgentMode = + GeckoSessionSettings.USER_AGENT_MODE_MOBILE + sessionDelegate!!.session.reload() + } else { + sessionDelegate!!.session.settings.userAgentMode = + GeckoSessionSettings.USER_AGENT_MODE_DESKTOP + sessionDelegate!!.session.reload() + } + } + bottomSheetDialog.dismiss() + + } + + var adapter = MenuAddonsAdapater() + binding.menuAddonsRecyclerView.layoutManager = + LinearLayoutManager(context, RecyclerView.HORIZONTAL, false); + binding.menuAddonsRecyclerView.adapter = adapter + GeckoRuntime.getDefault(context).webExtensionController.list().accept { + if (it != null) { + if (it.size != 0 && !isHome) + binding.addonContainer.visibility = View.VISIBLE + else + binding.addonContainer.visibility = View.GONE + var webExtensions = it + for (e in it) { + if (!e.metaData.enabled) + if (webExtensions != null) { + webExtensions = webExtensions.toMutableList().apply { remove(e) } + } + } + adapter.submitList(webExtensions) + } + } + + binding.addonIcon.setOnClickListener { + var intent = Intent(context, HolderActivity::class.java) + intent.putExtra("Page", "ADDONS") + context.startActivity(intent) + bottomSheetDialog.dismiss() + } + + } + + fun show() { + bottomSheetDialog.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ReceivedTabPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ReceivedTabPopup.kt new file mode 100644 index 0000000..459c5fd --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ReceivedTabPopup.kt @@ -0,0 +1,197 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewmodel.compose.viewModel +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.fxa.AccountManagerCollection +import org.mozilla.xiu.browser.fxa.TabReceivedViewModel +import org.mozilla.xiu.browser.session.createSession +import kotlinx.coroutines.launch +import mozilla.components.feature.accounts.push.SendTabFeature +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sync.SyncReason + +class ReceivedTabPopup : BottomSheetDialogFragment() { + private lateinit var accountManagerCollection : AccountManagerCollection + private lateinit var fxaAccountManager: FxaAccountManager + private lateinit var receivedTabPopupObervers: ReceivedTabPopupObervers + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(BottomSheetDialogFragment.STYLE_NORMAL, R.style.BottomSheetDialog) + accountManagerCollection = ViewModelProvider(requireActivity())[AccountManagerCollection::class.java] + receivedTabPopupObervers = ViewModelProvider(requireActivity())[ReceivedTabPopupObervers::class.java] + receivedTabPopupObervers.changeState(true) + lifecycleScope.launch { + accountManagerCollection.data.collect(){ + fxaAccountManager = it + + } + } + } + + @OptIn(ExperimentalFoundationApi::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + + + lifecycleScope.launch { + fxaAccountManager.syncNow(SyncReason.User) + fxaAccountManager + .authenticatedAccount() + ?.deviceConstellation() + ?.pollForCommands() + + } + + + + setContent { + MaterialTheme() { + val tabReceivedViewModel:TabReceivedViewModel = viewModel() + SendTabFeature(fxaAccountManager) { device, tabs -> + // handle tab data here. + tabReceivedViewModel.changeTabs(device!!,tabs) + } + val tabs = tabReceivedViewModel.tabs.collectAsState() + val device = tabReceivedViewModel.device.collectAsState() + ConstraintLayout{ + val (lottie,panel) = createRefs() + Loader(modifier = Modifier + .height(128.dp) + .width(128.dp) + .constrainAs(lottie) { + start.linkTo(parent.start) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom, 24.dp) + } + ) + + LazyColumn( + modifier = Modifier.constrainAs(panel){ + width = Dimension.fillToConstraints + bottom.linkTo(lottie.top,16.dp) + start.linkTo(parent.start,16.dp) + end.linkTo(parent.end,16.dp) + + }, + content = { + items(tabs.value.size){ + Card(modifier = Modifier.clickable { + createSession(tabs.value[it].url,requireActivity()) + dismiss() + + }) { + ConstraintLayout(modifier = Modifier.fillMaxSize()) { + val (icon,myDevice,title,url) = createRefs() + Image( + modifier = Modifier.height(24.dp).width(24.dp).constrainAs(icon){ + top.linkTo(parent.top,8.dp) + start.linkTo(parent.start,8.dp) + }, + painter = painterResource(id = R.drawable.pc_display), + contentDescription = "") + Text(text = device.value.displayName,modifier = Modifier + .basicMarquee() + .constrainAs(myDevice) { + width = Dimension.fillToConstraints + height = Dimension.fillToConstraints + top.linkTo(icon.top) + bottom.linkTo(icon.bottom) + start.linkTo(icon.end,8.dp) + end.linkTo(parent.end,8.dp) + + }) + Text(text = tabs.value[it].title, + modifier = Modifier + .basicMarquee() + .constrainAs(title) { + width = Dimension.fillToConstraints + height = Dimension.wrapContent + top.linkTo(myDevice.bottom,8.dp) + start.linkTo(parent.start,8.dp) + end.linkTo(parent.end,8.dp) + + + } + , + maxLines = 1) + Text(text = tabs.value[it].url, + modifier = Modifier + .basicMarquee() + .constrainAs(url) { + width = Dimension.fillToConstraints + height = Dimension.wrapContent + top.linkTo(title.bottom,8.dp) + start.linkTo(parent.start,8.dp) + end.linkTo(parent.end,8.dp) + bottom.linkTo(parent.bottom,8.dp) + } + , + maxLines = 1) + } + + } + } + }) + + + } + + } + } + + } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + receivedTabPopupObervers.changeState(false) + } + @Composable + fun Loader(modifier: Modifier) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.receive_wait_2)) + val progress by animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever) + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = modifier + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ReceivedTabPopupObervers.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ReceivedTabPopupObervers.kt new file mode 100644 index 0000000..d56e004 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/ReceivedTabPopupObervers.kt @@ -0,0 +1,13 @@ +package org.mozilla.xiu.browser.componets.popup + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class ReceivedTabPopupObervers :ViewModel(){ + private val _state = MutableStateFlow(false) + val state = _state.asStateFlow() + fun changeState(boolean: Boolean){ + _state.value = boolean + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/componets/popup/TabPopup.kt b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/TabPopup.kt new file mode 100644 index 0000000..1815cbd --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/componets/popup/TabPopup.kt @@ -0,0 +1,62 @@ +package org.mozilla.xiu.browser.componets.popup + +import android.view.LayoutInflater +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialog +import org.mozilla.xiu.browser.MainActivity +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.databinding.PopupTabBinding +import org.mozilla.xiu.browser.session.GeckoViewModel +import org.mozilla.xiu.browser.session.SeRuSettings +import org.mozilla.xiu.browser.tab.DelegateListLiveData +import org.mozilla.xiu.browser.tab.TabListAdapter +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession + +class TabPopup { + val context:MainActivity + private val bottomSheetDialog: BottomSheetDialog + private val binding:PopupTabBinding + val geckoViewModel: GeckoViewModel + constructor(context: MainActivity) { + this.context = context + bottomSheetDialog = BottomSheetDialog(context,R.style.BottomSheetDialog ) + binding = PopupTabBinding.inflate(LayoutInflater.from(context)) + bottomSheetDialog.setContentView(binding.root) + geckoViewModel= ViewModelProvider(context).get(GeckoViewModel::class.java) + + } + fun show(){ + val adapter=TabListAdapter() + binding.recyclerView.adapter=adapter + adapter.select=object :TabListAdapter.Select{ + override fun onSelect() { + bottomSheetDialog.dismiss() + } + + } + + binding.recyclerView.layoutManager = GridLayoutManager(context, 2); + DelegateListLiveData.getInstance().observe(context){ + adapter.submitList(it.toList()) + } + binding.popAddButton.setOnClickListener { + HomeLivedata.getInstance().Value(true) + bottomSheetDialog.dismiss() + } + + bottomSheetDialog.show() + } + fun createSession(uri: String) { + val session = GeckoSession() + val sessionSettings = session.settings + SeRuSettings(sessionSettings, context) + + session.open(GeckoRuntime.getDefault(context) ) + session.loadUri(uri) + geckoViewModel.changeSearch(session) + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/StageData.kt b/app/src/main/java/org/mozilla/xiu/browser/database/StageData.kt new file mode 100644 index 0000000..99cb3be --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/StageData.kt @@ -0,0 +1,36 @@ +package org.mozilla.xiu.browser.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room.databaseBuilder +import androidx.room.RoomDatabase +import org.mozilla.xiu.browser.database.bookmark.Bookmark +import org.mozilla.xiu.browser.database.bookmark.BookmarkDao +import org.mozilla.xiu.browser.database.history.History +import org.mozilla.xiu.browser.database.history.HistoryDao +import org.mozilla.xiu.browser.database.shortcut.Shortcut +import org.mozilla.xiu.browser.database.shortcut.ShortcutDao + +@Database(entities = [Bookmark::class, History::class,Shortcut::class], version = 1, exportSchema = false) +abstract class StageData : RoomDatabase() { + abstract val historyDao: HistoryDao? + abstract val bookmarkDao: BookmarkDao? + abstract val shortcutDao: ShortcutDao? + + companion object { + private var INSTANCE: StageData? = null + @JvmStatic + @Synchronized + fun getDatabase(context: Context): StageData? { + if (INSTANCE == null) { + INSTANCE = databaseBuilder( + context.applicationContext, + StageData::class.java, + "UserDatabase" + ) + .build() + } + return INSTANCE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/Bookmark.kt b/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/Bookmark.kt new file mode 100644 index 0000000..e64d1bb --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/Bookmark.kt @@ -0,0 +1,20 @@ +package org.mozilla.xiu.browser.database.bookmark + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +class Bookmark( + @field:ColumnInfo(name = "url_info") var url: String, + @field:ColumnInfo(name = "title_info") var title: String, + @field:ColumnInfo(name = "file_name") var file: String, + @field:ColumnInfo(name = "show_info") var show: Boolean +) { + @PrimaryKey(autoGenerate = true) + var id = 0 + + @ColumnInfo(name = "mix") + var mix: String = url + title + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkDao.kt b/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkDao.kt new file mode 100644 index 0000000..c2dbcba --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkDao.kt @@ -0,0 +1,31 @@ +package org.mozilla.xiu.browser.database.bookmark + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface BookmarkDao { + @Insert + fun insertBookmark(vararg bookmarks: Bookmark?) + + @Update + fun updateBookmark(vararg bookmarks: Bookmark?) + + @Delete + fun deleteBookmark(vararg bookmarks: Bookmark?) + + @Query("Delete FROM BOOKMARK") + fun deleteAllBookmark() + + @get:Query("SELECT * FROM Bookmark ORDER BY ID DESC") + val allBookmarksLive: LiveData?>? + + @Query("SELECT * FROM Bookmark WHERE title_info LIKE:pattern ORDER BY ID DESC") + fun findBookmarksWithPattern(pattern: String?): LiveData?>? + + @Query("SELECT * FROM Bookmark WHERE title_info LIKE:pattern ORDER BY ID DESC") + fun findBookmarksWithTitle(pattern: String?): LiveData?>? + + @Query("SELECT * FROM Bookmark WHERE show_info LIKE:pattern ORDER BY ID DESC") + fun findBookmarksWithShow(pattern: Boolean?): LiveData?>? +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkRepository.kt b/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkRepository.kt new file mode 100644 index 0000000..199b019 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkRepository.kt @@ -0,0 +1,77 @@ +package org.mozilla.xiu.browser.database.bookmark + +import android.content.Context +import android.os.AsyncTask +import androidx.lifecycle.LiveData +import org.mozilla.xiu.browser.database.StageData.Companion.getDatabase + +class BookmarkRepository internal constructor(context: Context) { + var bookmarkDao: BookmarkDao? + var allBookmarkLive: LiveData?>? + + init { + val stageData = getDatabase(context.applicationContext) + bookmarkDao = stageData!!.bookmarkDao + allBookmarkLive = bookmarkDao?.allBookmarksLive + } + + fun insertBookmark(vararg bookmarks: Bookmark?) { + InsertAsyncTask(bookmarkDao).execute(*bookmarks) + } + + fun updateBookmark(vararg bookmarks: Bookmark?) { + UpdateAsyncTask(bookmarkDao).execute(*bookmarks) + } + + fun deleteBookmark(vararg bookmarks: Bookmark?) { + DeleteAsyncTask(bookmarkDao).execute(*bookmarks) + } + + fun deleteAllbookmarks() { + DeleteAllAsyncTask(bookmarkDao).execute() + } + + fun findBookmarksWithPattern(pattern: String): LiveData?>? { + return bookmarkDao!!.findBookmarksWithPattern("%$pattern%") + } + + fun findBookmarksWithTitle(pattern: String?): LiveData?>? { + return bookmarkDao!!.findBookmarksWithTitle(pattern) + } + + fun findBookmarksWithShow(pattern: Boolean?): LiveData?>? { + return bookmarkDao!!.findBookmarksWithShow(pattern) + } + + internal class InsertAsyncTask(private val bookmarkDao: BookmarkDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Bookmark?): Void? { + bookmarkDao!!.insertBookmark(*params) + return null + } + } + + internal class UpdateAsyncTask(private val bookmarkDao: BookmarkDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Bookmark?): Void? { + bookmarkDao!!.updateBookmark(*params) + return null + } + } + + internal class DeleteAsyncTask(private val bookmarkDao: BookmarkDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Bookmark?): Void? { + bookmarkDao!!.deleteBookmark(*params) + return null + } + } + + internal class DeleteAllAsyncTask(private val bookmarkDao: BookmarkDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Void?): Void? { + bookmarkDao!!.deleteAllBookmark() + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkViewModel.kt b/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkViewModel.kt new file mode 100644 index 0000000..5b6d4f3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/bookmark/BookmarkViewModel.kt @@ -0,0 +1,44 @@ +package org.mozilla.xiu.browser.database.bookmark + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData + +class BookmarkViewModel(application: Application) : AndroidViewModel(application) { + var bookmarkRepository: BookmarkRepository + + init { + bookmarkRepository = BookmarkRepository(application) + } + + val allBookmarksLive: LiveData?>? + get() = bookmarkRepository.allBookmarkLive + + fun findBookmarksWithPattern(pattern: String): LiveData?>? { + return bookmarkRepository.findBookmarksWithPattern(pattern) + } + + fun findBookmarksWithTitle(pattern: String?): LiveData?>? { + return bookmarkRepository.findBookmarksWithTitle(pattern) + } + + fun findBookmarksWithShow(pattern: Boolean?): LiveData?>? { + return bookmarkRepository.findBookmarksWithShow(pattern) + } + + fun insertBookmarks(vararg bookmarks: Bookmark?) { + bookmarkRepository.insertBookmark(*bookmarks) + } + + fun updateBookmarks(vararg bookmarks: Bookmark?) { + bookmarkRepository.updateBookmark(*bookmarks) + } + + fun deleteBookmarks(vararg bookmarks: Bookmark?) { + bookmarkRepository.deleteBookmark(*bookmarks) + } + + fun deleteAllBookmarks() { + bookmarkRepository.deleteAllbookmarks() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/history/History.kt b/app/src/main/java/org/mozilla/xiu/browser/database/history/History.kt new file mode 100644 index 0000000..eb13354 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/history/History.kt @@ -0,0 +1,19 @@ +package org.mozilla.xiu.browser.database.history + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +class History( + @field:ColumnInfo(name = "url_info") var url: String, @field:ColumnInfo( + name = "title_info" + ) var title: String, @field:ColumnInfo(name = "time_info") var time: Int +) { + @PrimaryKey(autoGenerate = true) + var id = 0 + + @ColumnInfo(name = "mix") + var mix: String = url + title + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryDao.kt b/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryDao.kt new file mode 100644 index 0000000..6eca51e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryDao.kt @@ -0,0 +1,31 @@ +package org.mozilla.xiu.browser.database.history + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface HistoryDao { + @Insert + fun insertHistory(vararg histories: History?) + + @Update + fun updateHistory(vararg histories: History?) + + @Delete + fun deleteHistory(vararg histories: History?) + + @Query("DELETE FROM HISTORY") + fun deleteAllHistories() + + @get:Query("SELECT * FROM HISTORY ORDER BY ID DESC") + val allHistoriesLive: LiveData?>? + + @Query("SELECT * FROM History WHERE url_info LIKE:pattern ORDER BY ID DESC") + fun findHistoriesWithPattern(pattern: String?): LiveData?>? + + @Query("SELECT * FROM History WHERE title_info LIKE:pattern ORDER BY ID DESC") + fun findHistoriesWithTitle(pattern: String?): LiveData?>? + + @Query("SELECT * FROM History WHERE mix LIKE:pattern ORDER BY ID DESC") + fun findHistoriesWithMix(pattern: String?): LiveData?>? +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryRepository.kt b/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryRepository.kt new file mode 100644 index 0000000..b1ba5b9 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryRepository.kt @@ -0,0 +1,77 @@ +package org.mozilla.xiu.browser.database.history + +import android.content.Context +import android.os.AsyncTask +import androidx.lifecycle.LiveData +import org.mozilla.xiu.browser.database.StageData.Companion.getDatabase + +internal class HistoryRepository(context: Context) { + val allHistoriesLive: LiveData?>? + private val historyDao: HistoryDao? + + init { + val stageData = getDatabase(context.applicationContext) + historyDao = stageData!!.historyDao + allHistoriesLive = historyDao?.allHistoriesLive + } + + fun insertHistory(vararg histories: History?) { + InsertAsyncTask(historyDao).execute(*histories) + } + + fun updateHistory(vararg histories: History?) { + UpdateAsyncTask(historyDao).execute(*histories) + } + + fun deleteHistory(vararg histories: History?) { + DeleteAsyncTask(historyDao).execute(*histories) + } + + fun deleteAllHistories(vararg histories: History?) { + DeleteAllAsyncTask(historyDao).execute() + } + + fun findHistoriesWithPattern(pattern: String): LiveData?>? { + return historyDao!!.findHistoriesWithPattern("%$pattern%") + } + + fun findHistoriesWithTitle(pattern: String?): LiveData?>? { + return historyDao!!.findHistoriesWithTitle(pattern) + } + + fun findHistoriesWithMix(pattern: String?): LiveData?>? { + return historyDao!!.findHistoriesWithMix("%$pattern%") + } + + internal class InsertAsyncTask(private val historyDao: HistoryDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: History?): Void? { + historyDao!!.insertHistory(*params) + return null + } + } + + internal class UpdateAsyncTask(private val historyDao: HistoryDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: History?): Void? { + historyDao!!.updateHistory(*params) + return null + } + } + + internal class DeleteAsyncTask(private val historyDao: HistoryDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: History?): Void? { + historyDao!!.deleteHistory(*params) + return null + } + } + + internal class DeleteAllAsyncTask(private val historyDao: HistoryDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Void?): Void? { + historyDao!!.deleteAllHistories() + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryViewModel.kt b/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryViewModel.kt new file mode 100644 index 0000000..90c8f3f --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/history/HistoryViewModel.kt @@ -0,0 +1,44 @@ +package org.mozilla.xiu.browser.database.history + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData + +class HistoryViewModel(application: Application) : AndroidViewModel(application) { + private val historyRepository: HistoryRepository + + init { + historyRepository = HistoryRepository(application) + } + + val allHistoriesLive: LiveData?>? + get() = historyRepository.allHistoriesLive + + fun findHistoriesWithPattern(pattern: String): LiveData?>? { + return historyRepository.findHistoriesWithPattern(pattern) + } + + fun findHistoriesWithTitle(pattern: String?): LiveData?>? { + return historyRepository.findHistoriesWithTitle(pattern) + } + + fun findHistoriesWithMix(pattern: String?): LiveData?>? { + return historyRepository.findHistoriesWithMix(pattern) + } + + fun insertHistories(vararg histories: History?) { + historyRepository.insertHistory(*histories) + } + + fun updateHistories(vararg histories: History?) { + historyRepository.updateHistory(*histories) + } + + fun deleteHistories(vararg histories: History?) { + historyRepository.deleteHistory(*histories) + } + + fun deleteAllHistories() { + historyRepository.deleteAllHistories() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/Shortcut.kt b/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/Shortcut.kt new file mode 100644 index 0000000..cffbbbc --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/Shortcut.kt @@ -0,0 +1,19 @@ +package org.mozilla.xiu.browser.database.shortcut + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +class Shortcut( + @field:ColumnInfo(name = "url_info") var url: String, + @field:ColumnInfo(name = "title_info") var title: String, + @field:ColumnInfo(name = "time_info") var time: Int +) { + @PrimaryKey(autoGenerate = true) + var id = 0 + + @ColumnInfo(name = "mix") + var mix: String = url + title + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutDao.kt b/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutDao.kt new file mode 100644 index 0000000..a32e953 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutDao.kt @@ -0,0 +1,31 @@ +package org.mozilla.xiu.browser.database.shortcut + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface ShortcutDao { + @Insert + fun insertShortcut(vararg shortcuts: Shortcut?) + + @Update + fun updateShortcut(vararg shortcuts: Shortcut?) + + @Delete + fun deleteShortcut(vararg shortcuts: Shortcut?) + + @Query("DELETE FROM Shortcut") + fun deleteAllShortcuts() + + @get:Query("SELECT * FROM Shortcut ORDER BY ID DESC") + val allShortcutsLive: LiveData?>? + + @Query("SELECT * FROM Shortcut WHERE url_info LIKE:pattern ORDER BY ID DESC") + fun findShortcutsWithPattern(pattern: String?): LiveData?>? + + @Query("SELECT * FROM Shortcut WHERE title_info LIKE:pattern ORDER BY ID DESC") + fun findShortcutsWithTitle(pattern: String?): LiveData?>? + + @Query("SELECT * FROM Shortcut WHERE mix LIKE:pattern ORDER BY ID DESC") + fun findShortcutsWithMix(pattern: String?): LiveData?>? +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutRepository.kt b/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutRepository.kt new file mode 100644 index 0000000..d5f98fa --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutRepository.kt @@ -0,0 +1,77 @@ +package org.mozilla.xiu.browser.database.shortcut + +import android.content.Context +import android.os.AsyncTask +import androidx.lifecycle.LiveData +import org.mozilla.xiu.browser.database.StageData.Companion.getDatabase + +internal class ShortcutRepository(context: Context) { + val allShortcutsLive: LiveData?>? + private val shortcutDao: ShortcutDao? + + init { + val stageData = getDatabase(context.applicationContext) + shortcutDao = stageData!!.shortcutDao + allShortcutsLive = shortcutDao?.allShortcutsLive + } + + fun insertShortcut(vararg shortcuts: Shortcut?) { + InsertAsyncTask(shortcutDao).execute(*shortcuts) + } + + fun updateShortcut(vararg shortcuts: Shortcut?) { + UpdateAsyncTask(shortcutDao).execute(*shortcuts) + } + + fun deleteShortcut(vararg shortcuts: Shortcut?) { + DeleteAsyncTask(shortcutDao).execute(*shortcuts) + } + + fun deleteAllShortcuts(vararg shortcuts: Shortcut?) { + DeleteAllAsyncTask(shortcutDao).execute() + } + + fun findShortcutsWithPattern(pattern: String): LiveData?>? { + return shortcutDao!!.findShortcutsWithPattern("%$pattern%") + } + + fun findShortcutsWithTitle(pattern: String?): LiveData?>? { + return shortcutDao!!.findShortcutsWithTitle(pattern) + } + + fun findShortcutsWithMix(pattern: String?): LiveData?>? { + return shortcutDao!!.findShortcutsWithMix(pattern) + } + + internal class InsertAsyncTask(private val shortcutDao: ShortcutDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Shortcut?): Void? { + shortcutDao!!.insertShortcut(*params) + return null + } + } + + internal class UpdateAsyncTask(private val shortcutDao: ShortcutDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Shortcut?): Void? { + shortcutDao!!.updateShortcut(*params) + return null + } + } + + internal class DeleteAsyncTask(private val shortcutDao: ShortcutDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Shortcut?): Void? { + shortcutDao!!.deleteShortcut(*params) + return null + } + } + + internal class DeleteAllAsyncTask(private val shortcutDao: ShortcutDao?) : + AsyncTask() { + protected override fun doInBackground(vararg params: Void?): Void? { + shortcutDao!!.deleteAllShortcuts() + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutViewModel.kt b/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutViewModel.kt new file mode 100644 index 0000000..e901692 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/database/shortcut/ShortcutViewModel.kt @@ -0,0 +1,44 @@ +package org.mozilla.xiu.browser.database.shortcut + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData + +class ShortcutViewModel(application: Application) : AndroidViewModel(application) { + private val shortcutRepository: ShortcutRepository + + init { + shortcutRepository = ShortcutRepository(application) + } + + val allShortcutsLive: LiveData?>? + get() = shortcutRepository.allShortcutsLive + + fun findShortcutsWithPattern(pattern: String): LiveData?>? { + return shortcutRepository.findShortcutsWithPattern(pattern) + } + + fun findShortcutsWithTitle(pattern: String?): LiveData?>? { + return shortcutRepository.findShortcutsWithTitle(pattern) + } + + fun findShortcutsWithMix(pattern: String?): LiveData?>? { + return shortcutRepository.findShortcutsWithMix(pattern) + } + + fun insertShortcuts(vararg shortcuts: Shortcut?) { + shortcutRepository.insertShortcut(*shortcuts) + } + + fun updateShortcuts(vararg shortcuts: Shortcut?) { + shortcutRepository.updateShortcut(*shortcuts) + } + + fun deleteShortcuts(vararg shortcuts: Shortcut?) { + shortcutRepository.deleteShortcut(*shortcuts) + } + + fun deleteAllShortcuts() { + shortcutRepository.deleteAllShortcuts() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/DownListCallback.kt b/app/src/main/java/org/mozilla/xiu/browser/download/DownListCallback.kt new file mode 100644 index 0000000..84c6d45 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/DownListCallback.kt @@ -0,0 +1,22 @@ +package org.mozilla.xiu.browser.download + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil + +object DownListCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DownloadTask, newItem: DownloadTask): Boolean { + return oldItem.currentSize == newItem.currentSize + && oldItem.title == newItem.title + && oldItem.progress == newItem.progress + && oldItem.totalSize == newItem.totalSize + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: DownloadTask, newItem: DownloadTask): Boolean { + return oldItem.currentSize == newItem.currentSize + && oldItem.title == newItem.title + && oldItem.progress == newItem.progress + && oldItem.totalSize == newItem.totalSize + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/DownloadChooser.java b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadChooser.java new file mode 100644 index 0000000..d1f9192 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadChooser.java @@ -0,0 +1,37 @@ +package org.mozilla.xiu.browser.download; + +/** + * 作者:By hdy + * 日期:On 2019/1/28 + * 时间:At 14:53 + */ +public class DownloadChooser { + + public static String smartFilm(String fileName) { + return smartFilm(fileName, false); + } + + public static String smartFilm(String fileName, boolean includeVideo) { + if (fileName == null || fileName.isEmpty()) { + return ""; + } + if (UrlDetector.isVideoOrMusic(fileName)) { + if (!UrlDetector.isMusic(fileName)) { + return includeVideo ? "视频" : ""; + } else { + return "音乐/音频"; + } + } else if (UrlDetector.isImage(fileName)) { + return "图片"; + } else if (fileName.contains(".zip") || fileName.contains(".rar") || fileName.contains(".7z") || fileName.contains(".tar") || fileName.contains(".gz")) { + return "压缩包"; + } else if (fileName.contains(".txt") || fileName.contains(".epub") || fileName.contains(".azw3") || fileName.contains(".mobi") || fileName.contains(".pdf") + || fileName.contains(".doc") || fileName.contains(".xls") || fileName.contains(".json")) { + return "文档/电子书"; + } else if (fileName.contains(".apk") || fileName.contains(".exe") || fileName.contains(".hap") || fileName.contains(".msi") + || fileName.contains(".dmg") || fileName.contains(".xpi") || fileName.contains(".crx")) { + return "安装包"; + } + return "其它格式"; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/DownloadFactory.kt b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadFactory.kt new file mode 100644 index 0000000..e9c4b71 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadFactory.kt @@ -0,0 +1,61 @@ +package org.mozilla.xiu.browser.download + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import okhttp3.Response +import rxhttp.wrapper.callback.UriFactory +import rxhttp.wrapper.utils.query +import java.io.File + +class Android10DownloadFactory @JvmOverloads constructor( + context: Context, + private val filename: String, + private val relativePath: String = Environment.DIRECTORY_DOWNLOADS +) : UriFactory(context) { + + /** + * [MediaStore.Files.getContentUri] + * [MediaStore.Downloads.EXTERNAL_CONTENT_URI] + * [MediaStore.Audio.Media.EXTERNAL_CONTENT_URI] + * [MediaStore.Video.Media.EXTERNAL_CONTENT_URI] + * [MediaStore.Images.Media.EXTERNAL_CONTENT_URI] + */ + @RequiresApi(Build.VERSION_CODES.Q) + fun getInsertUri() = MediaStore.Downloads.EXTERNAL_CONTENT_URI + + override fun query(): Uri? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + getInsertUri().query(context, filename, relativePath) + } else { + val file = File("${Environment.getExternalStorageDirectory()}/$relativePath/$filename") + Uri.fromFile(file) + } + } + + override fun insert(response: Response): Uri { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val uri = getInsertUri().query(context, filename, relativePath) + /* + * 通过查找,要插入的Uri已经存在,就无需再次插入 + * 否则会出现新插入的文件,文件名被系统更改的现象,因为insert不会执行覆盖操作 + */ + if (uri != null) return uri + ContentValues().run { + put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) //下载到指定目录 + put(MediaStore.MediaColumns.DISPLAY_NAME, filename) //文件名 + //取contentType响应头作为文件类型 + put(MediaStore.MediaColumns.MIME_TYPE, response.body?.contentType().toString()) + context.contentResolver.insert(getInsertUri(), this) + //当相同路径下的文件,在文件管理器中被手动删除时,就会插入失败 + } ?: throw NullPointerException("Uri insert failed. Try changing filename") + } else { + val file = File("${Environment.getExternalStorageDirectory()}/$relativePath/$filename") + Uri.fromFile(file) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/DownloadFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadFragment.kt new file mode 100644 index 0000000..3d9ea07 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadFragment.kt @@ -0,0 +1,56 @@ +package org.mozilla.xiu.browser.download + +import android.content.Intent +import android.os.Bundle +import android.os.Environment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import org.mozilla.xiu.browser.databinding.FragmentDownloadBinding + + +/** + * A simple [Fragment] subclass. + * Use the [DownloadFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class DownloadFragment : Fragment() { + private var _binding: FragmentDownloadBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + _binding = FragmentDownloadBinding.inflate(inflater, container, false) + var adapter = DownloadListAdapter() + binding.DownloadRecyclerView.layoutManager = LinearLayoutManager(context); + binding.DownloadRecyclerView.adapter = adapter + DownloadTaskLiveData.getInstance().observe(viewLifecycleOwner) { + adapter.submitList(it.toList()) + } + binding.localButton4.setOnClickListener { + val intent = Intent(context, FileBrowserActivity::class.java) + val relativePath: String = Environment.DIRECTORY_DOWNLOADS + intent.putExtra( + "path", + "${Environment.getExternalStorageDirectory()}/$relativePath" + ) + startActivity(intent) + } + return binding.root + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/DownloadListAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadListAdapter.kt new file mode 100644 index 0000000..4994c0a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadListAdapter.kt @@ -0,0 +1,64 @@ +package org.mozilla.xiu.browser.download + +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.RequiresApi +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.databinding.ItemDownloadBinding +import org.mozilla.xiu.browser.utils.ShareUtil +import rxhttp.wrapper.utils.query + + +class DownloadListAdapter : ListAdapter(DownListCallback) { + private val relativePath: String = Environment.DIRECTORY_DOWNLOADS + @RequiresApi(Build.VERSION_CODES.Q) + fun getInsertUri() = MediaStore.Downloads.EXTERNAL_CONTENT_URI + + inner class ItemTestViewHolder(private val binding: ItemDownloadBinding): RecyclerView.ViewHolder(binding.root){ + @RequiresApi(Build.VERSION_CODES.Q) + fun bind(bean:DownloadTask, mContext: Context){ + binding.task=getItem(adapterPosition) + binding.downloadButton2.setOnClickListener{ + if(bean.state==0) + bean.open() + else + bean.pause() + } + binding.materialCardView5.setOnClickListener { + if(bean.state==2){ + getInsertUri().query(mContext, bean.filename, relativePath) + ?.let { it1 -> open(mContext, it1) } + } + + } + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemDownloadBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + + fun open(context: Context, uri: Uri) { + val intent = Intent(Intent.ACTION_VIEW) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.setDataAndType(uri, "*/*"); + ShareUtil.findChooser(context, intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/DownloadTask.kt b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadTask.kt new file mode 100644 index 0000000..9465851 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadTask.kt @@ -0,0 +1,195 @@ +package org.mozilla.xiu.browser.download + +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.ContentResolver +import android.content.Context +import android.content.Context.NOTIFICATION_SERVICE +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import android.util.Log +import android.widget.RemoteViews +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import org.mozilla.xiu.browser.BR +import org.mozilla.xiu.browser.HolderActivity +import org.mozilla.xiu.browser.R +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.launch +import org.mozilla.xiu.browser.App +import org.mozilla.xiu.browser.utils.ToastMgr +import org.mozilla.xiu.browser.webextension.WebExtensionsAddEvent +import org.mozilla.xiu.browser.webextension.WebextensionSession +import rxhttp.RxHttpPlugins +import rxhttp.toDownloadFlow +import rxhttp.wrapper.param.RxHttp +import java.io.File +import org.greenrobot.eventbus.EventBus +import org.mozilla.xiu.browser.utils.UriUtilsPro + + +class DownloadTask : BaseObservable { + @get:Bindable + var title: String = "" + + @get:Bindable + var currentSize: Long = 0 + + @get:Bindable + var totalSize: Long = 0 + + @get:Bindable + var progress: Int = 0 + + @Bindable + var state: Int = 0 + + @get:Bindable + var text: String = "" + + private var mContext: Context + var uri: String + private var downloadFactory: Android10DownloadFactory + var filename: String + private lateinit var notificationManager: NotificationManager + private lateinit var customNotification: Notification + + constructor(mContext: Context, uri: String, filename: String) { + this.mContext = mContext + this.uri = uri + this.filename = filename + title = filename + notifyPropertyChanged(BR.title) + downloadFactory = Android10DownloadFactory(mContext, filename) + notificationManager = mContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + if (!notificationManager.areNotificationsEnabled()) { + Toast.makeText(mContext, "请先授予通知权限", Toast.LENGTH_SHORT).show() + val intent = Intent() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, "org.mozilla.xiu.browser") + intent.addFlags(FLAG_ACTIVITY_NEW_TASK) + mContext.startActivity(intent) + } + } + } + + fun open() { + (mContext as FragmentActivity).lifecycleScope.launch { + RxHttp.get(uri) + .tag(uri) + .toDownloadFlow(downloadFactory, true) { + progress = it.progress //当前进度 0-100 + currentSize = it.currentSize / 1024 / 1024 //当前已下载的字节大小 + totalSize = it.totalSize / 1024 / 1024 //要下载的总字节大小 + android.util.Log.d("下载进度", "" + progress) + text = "已下载$currentSize MB•共$totalSize MB $progress%" + + if (progress != 100) { + var intent = Intent(mContext, HolderActivity::class.java).apply { + putExtra("Page", "DOWNLOAD") + } + initCustomNotification("正在进行下载任务", R.drawable.download, intent) + notificationManager.notify(5, customNotification) + } + notifyChange() + + }.catch { + }.collect { + open(mContext, it) + val resolver: ContentResolver = mContext.contentResolver + val intent = Intent(Intent.ACTION_VIEW) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.setDataAndType(it, resolver.getType(it)); + initCustomNotification("下载任务已完成", R.drawable.checkmark_circle, intent) + notificationManager.notify(5, customNotification) + } + + } + state = 1 + notifyPropertyChanged(BR.state) + + } + + fun pause() { + RxHttpPlugins.cancelAll(uri) + state = 0 + notifyPropertyChanged(BR.state) + } + + fun open(context: Context, uri: Uri) { + state = 2 + notifyPropertyChanged(BR.state) + val relativePath: String = Environment.DIRECTORY_DOWNLOADS + val name = UriUtilsPro.getFileName(uri) + val file = File("${Environment.getExternalStorageDirectory()}/$relativePath/$name") + Log.d("open", "open: " + file.absolutePath + ", exist: " + file.exists()) + if (file.exists() && (name.contains(".crx") || name.contains(".xpi"))) { + EventBus.getDefault().post(WebExtensionsAddEvent(file.absolutePath)) + } + ToastMgr.shortBottomCenter(App.application, title + "下载完成") + } + + /** + * 创建通知渠道 + */ + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(channelId: String, channelName: String, importance: Int) = + notificationManager.createNotificationChannel( + NotificationChannel( + channelId, + channelName, + importance + ) + ) + + @SuppressLint("RemoteViewLayout") + private fun initCustomNotification(title: String, icon: Int, intent: Intent) { + //RemoteView + val remoteViews = + RemoteViews("org.mozilla.xiu.browser", R.layout.custom_download_notification) + remoteViews.setTextViewText(R.id.textView33, title) + + val pendingIntent = PendingIntent.getActivity( + mContext, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + customNotification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel( + "custom", + "自定义通知", + NotificationManagerCompat.IMPORTANCE_DEFAULT + ) + NotificationCompat.Builder(mContext, "custom") + } else { + NotificationCompat.Builder(mContext) + }.apply { + setSmallIcon(icon)//小图标(显示在状态栏) + setCustomContentView(remoteViews)//设置自定义内容视图 + setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + setOnlyAlertOnce(true) + setOngoing(true) + setContentIntent(pendingIntent) + setAutoCancel(true) + }.build() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/DownloadTaskLiveData.kt b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadTaskLiveData.kt new file mode 100644 index 0000000..374f6e8 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadTaskLiveData.kt @@ -0,0 +1,23 @@ +package org.mozilla.xiu.browser.download + +import androidx.lifecycle.LiveData + +class DownloadTaskLiveData : LiveData>() { + fun Value(downloadTasks: ArrayList){ + postValue(downloadTasks) + } + override fun onActive() { + super.onActive() + } + + override fun onInactive() { + super.onInactive() + } + companion object { + private lateinit var globalData: DownloadTaskLiveData + fun getInstance(): DownloadTaskLiveData { + globalData = if (Companion::globalData.isInitialized) globalData else DownloadTaskLiveData() + return globalData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/DownloadUtil.kt b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadUtil.kt new file mode 100644 index 0000000..93a472a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/DownloadUtil.kt @@ -0,0 +1,63 @@ +package org.mozilla.xiu.browser.download + +import org.mozilla.xiu.browser.utils.StringUtil +import org.mozilla.xiu.browser.utils.contentdisposition.ContentDispositionHolder +import java.io.UnsupportedEncodingException +import java.net.URLDecoder + +/** + * 作者:By 15968 + * 日期:On 2023/10/15 + * 时间:At 21:06 + */ + +/** + * 解析文件名 + * + * @param dispositionHeader + * @return + */ +fun getDispositionFileName(dispositionHeader: String?): String { + var dispositionHeader = dispositionHeader + try { + val holder = ContentDispositionHolder(dispositionHeader) + var name: String = holder.filename + if (holder.parseException == null && StringUtil.isNotEmpty(name)) { + name = decodeUrl(name, "UTF-8")!!.trim { it <= ' ' } + return if (name.contains("UTF-8''")) { + if (name.endsWith("UTF-8''")) { + name.split("UTF-8''".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + } else { + name.split("UTF-8''".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1] + } + } else name + } + } catch (e: Throwable) { + e.printStackTrace() + } + if (!dispositionHeader.isNullOrEmpty()) { + dispositionHeader = + dispositionHeader.replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$".toRegex(), "$1") + val strings = dispositionHeader.split(";".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + if (strings.size > 1) { + dispositionHeader = strings[0] + } + if (dispositionHeader.contains(".") && !dispositionHeader.contains("filename=")) { + return decodeUrl(dispositionHeader, "UTF-8")!!.trim { it <= ' ' } + } + } + return "" +} + +fun decodeUrl(str: String?, code: String): String? { //url解码 + var str = str + try { + str = str!!.replace("%(?![0-9a-fA-F]{2})".toRegex(), "%25") + str = str.replace("\\+".toRegex(), "%2B") + str = URLDecoder.decode(str, code) + } catch (e: UnsupportedEncodingException) { + e.printStackTrace() + } + return str +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/FileBrowserActivity.java b/app/src/main/java/org/mozilla/xiu/browser/download/FileBrowserActivity.java new file mode 100644 index 0000000..0e405a4 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/FileBrowserActivity.java @@ -0,0 +1,237 @@ +package org.mozilla.xiu.browser.download; + +import android.os.Bundle; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.lxj.xpopup.XPopup; + +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.mozilla.xiu.browser.R; +import org.mozilla.xiu.browser.base.BaseSlideActivity; +import org.mozilla.xiu.browser.utils.ClipboardUtil; +import org.mozilla.xiu.browser.utils.DisplayUtil; +import org.mozilla.xiu.browser.utils.FileUtil; +import org.mozilla.xiu.browser.utils.MyStatusBarUtil; +import org.mozilla.xiu.browser.utils.ShareUtil; +import org.mozilla.xiu.browser.utils.StringUtil; +import org.mozilla.xiu.browser.utils.TimeUtil; +import org.mozilla.xiu.browser.utils.ToastMgr; +import org.mozilla.xiu.browser.utils.UriUtilsPro; +import org.mozilla.xiu.browser.webextension.WebExtensionsAddEvent; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * 作者:By 15968 + * 日期:On 2021/2/8 + * 时间:At 11:44 + */ + +public class FileBrowserActivity extends BaseSlideActivity { + private String rootPath; + private TextView curPathTextView; + private RecyclerView recyclerView; + private FileListAdapter adapter; + private String parentPath; + private static final String[] txt = new String[]{"json", "txt", "html", "js", "css", "log"}; + private static final String[] video = new String[]{"mp4", "mp3", "ts", "m3u8", "m4a"}; + + public FileBrowserActivity() { + } + + @Override + protected int initLayout(Bundle savedInstanceState) { + return R.layout.activity_file_browser; + } + + @Override + protected View getBackgroundView() { + return findView(R.id.ad_list_window); + } + + @Override + protected void initView2() { + int marginTop = MyStatusBarUtil.getStatusBarHeight(getContext()) + DisplayUtil.dpToPx(getContext(), 86); + View bg = findView(R.id.ad_list_bg); + findView(R.id.ad_list_window).setOnClickListener(view -> finish()); + LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) bg.getLayoutParams(); + layoutParams.topMargin = marginTop; + bg.setLayoutParams(layoutParams); + curPathTextView = findViewById(R.id.curPath); + findView(R.id.back_icon).setOnClickListener(v -> finish()); + recyclerView = findView(R.id.recycler_view); + recyclerView.setItemAnimator(new DefaultItemAnimator()); + } + + @Override + protected void initData(Bundle savedInstanceState) { + rootPath = UriUtilsPro.getRootDir(getContext()); + parentPath = rootPath; + adapter = new FileListAdapter(getContext(), new ArrayList<>()); + adapter.setOnItemClickListener(new FileListAdapter.OnItemClickListener() { + + @Override + public void onClick(View view, int position) { + File file = adapter.getList().get(position); + if ("b1".equals(file.getName())) { + getFileDir(rootPath); + } else if ("b2".equals(file.getName())) { + getFileDir(parentPath); + } else if (file.isDirectory()) { + getFileDir(file.getPath()); + } else { + String extension = FileUtil.getExtension(file.getName()); + //网页 + if ("xpi".equalsIgnoreCase(extension) || "crx".equalsIgnoreCase(extension)) { + new XPopup.Builder(getContext()) + .asCenterList(null, new String[]{"安装扩展程序", "外部软件打开"}, null, 100, (po, te) -> { + if (po == 0) { + EventBus.getDefault().post(new WebExtensionsAddEvent(file.getAbsolutePath())); + finish(); + } else { + ShareUtil.findChooserToDeal(getContext(), file.getAbsolutePath()); + } + }).show(); + return; + } + ShareUtil.findChooserToDeal(getContext(), file.getAbsolutePath()); + } + } + + @Override + public void onLongClick(View view, int position) { + File file = adapter.getList().get(position); + if ("b1".equals(file.getName()) || "b2".equals(file.getName())) { + return; + } + String[] titles; + if (file.isDirectory()) { + titles = new String[]{"目录详情", "删除目录", "复制路径"}; + } else { + titles = new String[]{"分享文件", "外部打开", "文件详情", "删除文件", "复制路径"}; + } + new XPopup.Builder(getContext()) + .asCenterList("选择操作", titles, (position1, text) -> { + switch (text) { + case "分享文件": + ShareUtil.findChooserToSend(getContext(), file.getAbsolutePath()); + break; + case "外部打开": + ShareUtil.findChooserToDeal(getContext(), "file://" + file.getAbsolutePath()); + break; + case "复制路径": + ClipboardUtil.copyToClipboardForce(getContext(), file.getAbsolutePath()); + break; + case "目录详情": + String size1 = FileUtil.getFormatedFileSize(FileUtil.getFolderSize(file)); + File[] children = file.listFiles(); + String[] list1 = new String[]{ + "目录名称:" + file.getName(), + "目录大小:" + size1, + "修改时间:" + TimeUtil.formatTime(file.lastModified()), + "子文件数:" + (children == null ? 0 : children.length) + }; + new XPopup.Builder(getContext()) + .asCustom(new FileDetailPopup(FileBrowserActivity.this, text, list1)).show(); + break; + case "文件详情": + String size = FileUtil.getFormatedFileSize(FileUtil.getFolderSize(file)); + String[] list = new String[]{ + "文件名称:" + file.getName(), + "文件大小:" + size, + "修改时间:" + TimeUtil.formatTime(file.lastModified()) + }; + new XPopup.Builder(getContext()) + .asCustom(new FileDetailPopup(FileBrowserActivity.this, text, list)).show(); + break; + case "删除目录": + new XPopup.Builder(getContext()) + .asConfirm("温馨提示", "确认删除该目录下所有文件吗?注意删除后无法恢复!", () -> { + FileUtil.deleteDirs(file.getAbsolutePath()); + ToastMgr.shortBottomCenter(getContext(), "目录已删除"); + getFileDir(curPathTextView.getText().toString()); + }).show(); + break; + case "删除文件": + new XPopup.Builder(getContext()) + .asConfirm("温馨提示", "确认删除该文件吗?注意删除后无法恢复!", () -> { + FileUtil.deleteFile(file.getAbsolutePath()); + ToastMgr.shortBottomCenter(getContext(), "文件已删除"); + getFileDir(curPathTextView.getText().toString()); + }).show(); + break; + } + }).show(); + } + }); + GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 1); + recyclerView.setLayoutManager(gridLayoutManager); + recyclerView.setAdapter(adapter); + getFileDir(rootPath); + + String dataPath = getIntent().getStringExtra("path"); + if (StringUtil.isNotEmpty(dataPath)) { + getFileDir(dataPath); + } else { + getFileDir(rootPath); + } + } + + private void getFileDir(String filePath) { + curPathTextView.setText(filePath); + List itemsList = adapter.getList(); + itemsList.clear(); + File file = new File(filePath); + File[] files = file.listFiles(); + if (!filePath.equals("/")) { + File file1 = new File("b1"); + itemsList.add(file1); + File file2 = new File("b2"); + itemsList.add(file2); + parentPath = file.getParent(); + } else { + parentPath = filePath; + } + if (files == null) { + files = new File[]{}; + } + List fileList = new ArrayList<>(Arrays.asList(files)); + Collections.sort(fileList, (o1, o2) -> { + if (StringUtils.isEmpty(o1.getName())) { + return -1; + } + if (StringUtils.isEmpty(o2.getName())) { + return 1; + } + if (o1.isDirectory() && o2.isFile()) { + return -1; + } + if (o1.isFile() && o2.isDirectory()) { + return 1; + } + return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase()); + }); + itemsList.addAll(fileList); + adapter.notifyDataSetChanged(); + } + + @Override + public void onBackPressed() { + if (StringUtils.equals(curPathTextView.getText(), rootPath) || StringUtils.equals(curPathTextView.getText(), getIntent().getStringExtra("path"))) { + super.onBackPressed(); + } else { + getFileDir(parentPath); + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/FileDetailAdapter.java b/app/src/main/java/org/mozilla/xiu/browser/download/FileDetailAdapter.java new file mode 100644 index 0000000..2b5a37b --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/FileDetailAdapter.java @@ -0,0 +1,78 @@ +package org.mozilla.xiu.browser.download; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.mozilla.xiu.browser.R; + +import java.util.List; + +/** + * 作者:By hdy + * 日期:On 2017/9/10 + * 时间:At 17:26 + */ + +public class FileDetailAdapter extends RecyclerView.Adapter { + private Context context; + + public List getList() { + return list; + } + + private List list; + private OnClickListener clickListener; + + public FileDetailAdapter(Context context, List list, OnClickListener clickListener) { + this.context = context; + this.list = list; + this.clickListener = clickListener; + } + + public interface OnClickListener { + void click(String text); + void longClick(View view, String text); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new TitleHolder(LayoutInflater.from(context).inflate(R.layout.item_file_detail, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, final int position) { + if (viewHolder instanceof TitleHolder) { + TitleHolder holder = (TitleHolder) viewHolder; + String title = list.get(position); + holder.title.setText(title); + holder.bg.setOnClickListener(v -> clickListener.click(list.get(holder.getAdapterPosition()))); + holder.bg.setOnLongClickListener(v -> { + clickListener.longClick(v, list.get(holder.getAdapterPosition())); + return true; + }); + } + } + + @Override + public int getItemCount() { + return list.size(); + } + + private class TitleHolder extends RecyclerView.ViewHolder { + TextView title; + View bg; + + TitleHolder(View itemView) { + super(itemView); + bg = itemView.findViewById(R.id.bg); + title = itemView.findViewById(R.id.textView); + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/FileDetailPopup.java b/app/src/main/java/org/mozilla/xiu/browser/download/FileDetailPopup.java new file mode 100644 index 0000000..4f33bde --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/FileDetailPopup.java @@ -0,0 +1,121 @@ +package org.mozilla.xiu.browser.download; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.lxj.xpopup.core.BasePopupView; +import com.lxj.xpopup.core.BottomPopupView; +import com.lxj.xpopup.util.XPopupUtils; + +import org.mozilla.xiu.browser.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 作者:By 15968 + * 日期:On 2020/3/23 + * 时间:At 21:08 + */ +public class FileDetailPopup extends BottomPopupView { + + private List operations; + private String title; + private FileDetailAdapter.OnClickListener clickListener; + private FileDetailAdapter adapter; + + public FileDetailPopup(@NonNull Context context) { + super(context); + } + + public FileDetailPopup(@NonNull Activity activity, String title, String[] operations) { + super(activity); + this.title = title; + this.operations = new ArrayList<>(); + this.operations.addAll(Arrays.asList(operations)); + } + + public FileDetailPopup(@NonNull Activity activity, String title, List operations) { + super(activity); + this.title = title; + this.operations = operations; + } + + // 返回自定义弹窗的布局 + @Override + protected int getImplLayoutId() { + return R.layout.view_setting_menu_popup; + } + + // 执行初始化操作,比如:findView,设置点击,或者任何你弹窗内的业务逻辑 + @Override + protected void onCreate() { + super.onCreate(); + + TextView textView = findViewById(R.id.textView); + textView.setText(title); + + RecyclerView recyclerView = findViewById(R.id.recyclerView); + + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext()); + recyclerView.setLayoutManager(linearLayoutManager); + adapter = new FileDetailAdapter(getContext(), operations, new FileDetailAdapter.OnClickListener() { + @Override + public void click(String text) { + if(clickListener != null){ + clickListener.click(text); + } + } + + @Override + public void longClick(View view, String text) { + if(clickListener != null){ + clickListener.longClick(view, text); + } + } + }); + recyclerView.setAdapter(adapter); + } + + public void updateData(String[] operations){ + this.operations.clear(); + this.operations.addAll(Arrays.asList(operations)); + adapter.notifyDataSetChanged(); + } + + @Override + public BasePopupView show() { + return super.show(); + } + + + @Override + protected int getPopupHeight() { + return (int) (XPopupUtils.getScreenHeight(getContext()) * .75f); + } + + + public FileDetailPopup withClickListener(FileDetailAdapter.OnClickListener clickListener) { + this.clickListener = clickListener; + return this; + } + + public FileDetailAdapter.OnClickListener getClickListener() { + return clickListener; + } + + public void setClickListener(FileDetailAdapter.OnClickListener clickListener) { + this.clickListener = clickListener; + } + + public interface OnItemClickListener { + void onClick(String text); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/FileListAdapter.java b/app/src/main/java/org/mozilla/xiu/browser/download/FileListAdapter.java new file mode 100644 index 0000000..4a69131 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/FileListAdapter.java @@ -0,0 +1,137 @@ +package org.mozilla.xiu.browser.download; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; + +import org.mozilla.xiu.browser.R; + +import java.io.File; +import java.util.List; + +/** + * 作者:By hdy + * 日期:On 2017/9/10 + * 时间:At 17:26 + */ + +class FileListAdapter extends RecyclerView.Adapter { + private Context context; + + public List getList() { + return list; + } + + private List list; + private OnItemClickListener onItemClickListener; + + FileListAdapter(Context context, List list) { + this.context = context; + this.list = list; + } + + interface OnItemClickListener { + void onClick(View view, int position); + + void onLongClick(View view, int position); + } + + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + this.onItemClickListener = onItemClickListener; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new AvatarHolder(LayoutInflater.from(context).inflate(R.layout.item_file, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, final int position) { + if (viewHolder instanceof AvatarHolder) { + AvatarHolder holder = (AvatarHolder) viewHolder; + File file = list.get(position); + if ("b1".equals(file.getName())) { + holder.textView.setText("返回根目录.."); + updateImg(holder.imageView, R.drawable.icon_back); + } else if ("b2".equals(file.getName())) { + holder.textView.setText("返回上一层.."); + updateImg(holder.imageView, R.drawable.icon_back02); + } else { + holder.textView.setText(file.getName()); + if (file.isDirectory()) { + updateImg(holder.imageView, R.drawable.icon_folder3); + } else { + String type = DownloadChooser.smartFilm(file.getName(), true); + int id = R.drawable.icon_unknown; + if ("安装包".equals(type)) { + id = R.drawable.icon_app3; + } else if ("压缩包".equals(type)) { + id = R.drawable.icon_zip2; + } else if ("音乐/音频".equals(type)) { + id = R.drawable.icon_music3; + } else if ("文档/电子书".equals(type)) { + id = R.drawable.icon_txt2; + } else if ("其它格式".equals(type)) { + //id = R.drawable.icon_unknown; + } else if ("图片".equals(type)) { + id = R.drawable.icon_pic3; + } else if ("视频".equals(type)) { + id = R.drawable.icon_video2; + } + updateImg(holder.imageView, id); + } + } + + holder.resultBg.setOnClickListener(v -> { + if (onItemClickListener != null) { + if (holder.getAdapterPosition() >= 0) { + onItemClickListener.onClick(v, holder.getAdapterPosition()); + } + } + }); + + holder.resultBg.setOnLongClickListener(v -> { + if (onItemClickListener != null) { + if (holder.getAdapterPosition() >= 0) { + onItemClickListener.onLongClick(v, holder.getAdapterPosition()); + } + } + return true; + }); + } + } + + private void updateImg(ImageView imageView, int id) { + Glide.with(context) + .load(id) + .into(imageView); + } + + @Override + public int getItemCount() { + return list.size(); + } + + + private static class AvatarHolder extends RecyclerView.ViewHolder { + View resultBg; + ImageView imageView; + TextView textView; + + AvatarHolder(View itemView) { + super(itemView); + resultBg = itemView.findViewById(R.id.bg); + imageView = itemView.findViewById(R.id.item_reult_img); + textView = itemView.findViewById(R.id.textView); + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/download/UrlDetector.java b/app/src/main/java/org/mozilla/xiu/browser/download/UrlDetector.java new file mode 100644 index 0000000..20de00f --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/download/UrlDetector.java @@ -0,0 +1,203 @@ +package org.mozilla.xiu.browser.download; + +import org.apache.commons.lang3.StringUtils; +import org.mozilla.xiu.browser.utils.CollectionUtil; +import org.mozilla.xiu.browser.utils.StringUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * 作者:By 15968 + * 日期:On 2023/10/19 + * 时间:At 11:52 + */ + +public class UrlDetector { + + private static List apps = CollectionUtil.asList(".css", ".html", ".js", ".apk", ".apks", ".apk.1", ".exe", ".zip", ".rar", ".7z", ".hap", ".mtz"); + private static List htmls = CollectionUtil.asList(".css", ".html", ".js", ".apk", ".exe"); + public static List videos = CollectionUtil.asList(".mp4", ".MP4", ".m3u8", ".flv", ".avi", ".3gp", "mpeg", ".wmv", ".mov", ".MOV", "rmvb", ".dat", ".mkv", "qqBFdownload", "mime=video%2F", "=video_mp4"); + private static List musics = CollectionUtil.asList(".mp3", ".wav", ".ogg", ".flac", ".m4a"); + private static List images = CollectionUtil.asList(".ico", ".png", ".PNG", ".jpg", ".JPG", ".jpeg", ".JPEG", ".gif", ".GIF", ".webp", ".svg"); + private static List blockUrls = CollectionUtil.asList(".php?url=http", "/?url=http"); + private static List makeSureNotVideoRules = CollectionUtil.asList(".mp4.jp", ".mp4.png"); + + public static List getVideoRules() { + return videoRules; + } + + private static List videoRules = Collections.synchronizedList(new ArrayList<>()); + + public static String getNeedCheckUrl(String url) { + url = url.replace("http://", "").replace("https://", ""); + if (!url.contains("/")) { + return url; + } + String[] urls = url.split("/"); + if (urls.length > 1) { + //去掉域名 + return StringUtil.listToString(Arrays.asList(urls), 1, "/"); + } else if (urls.length < 1) { + return url; + } else if ((urls[0] + "/").equals(url)) { + return ""; + } + return url; + } + + public static boolean isVideoOrMusic(String url) { + if (StringUtil.isEmpty(url)) { + return false; + } + if (url.contains("isVideo=true") || url.contains("isMusic=true")) { + return true; + } + if (url.contains("ignoreVideo=true") || url.contains("#ignoreMusic=true#")) { + return false; + } + if (url.startsWith("x5Play://")) { + return true; + } + if (url.contains("@rule=") || url.contains("@lazyRule=")) { + return false; + } + if (url.startsWith("rtmp://")) { + return true; + } else if (url.startsWith("rtsp://") || url.contains("video://")) { + return true; + } + url = getNeedCheckUrl(url); + for (String html : makeSureNotVideoRules) { + //拦截掉 + if (url.contains(html)) { + return false; + } + } + for (String rule : videoRules) { + try { + if (Pattern.matches(rule.split("@domain=")[0], url)) { + return true; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + for (String html : blockUrls) { + //拦截掉 + if (url.contains(html)) { + return false; + } + } + for (String app : apps) { + if (url.endsWith(app)) { + return false; + } + } + for (String music : musics) { + if (url.contains(music)) { + return true; + } + } + for (String music : videos) { + if (url.contains(music)) { + return true; + } + } + return false; + } + + + public static boolean isImage(String url) { + if (StringUtil.isEmpty(url)) { + return false; + } + if (url.startsWith("x5Play://")) { + return false; + } + if (url.contains("@rule=") || url.contains("@lazyRule=") || url.contains("isVideo=true")) { + return false; + } +// if (ImageUrlMapEnum.getIdByUrl(url) > 0) { +// return true; +// } + for (String app : apps) { + if (url.endsWith(app)) { + return false; + } + } + url = StringUtil.removeDom(url); + if (url.contains("ignoreImg=true")) { + return false; + } + for (String image : images) { + if (url.contains(image)) { + return true; + } + } + return false; + } + + public static boolean isMusic(String url) { + if (StringUtil.isEmpty(url)) { + return false; + } + if (url.contains("@rule=") || url.contains("@lazyRule=") || url.contains("#ignoreMusic=true#")) { + return false; + } + if (url.contains("isMusic=true")) { + return true; + } + for (String app : apps) { + if (url.endsWith(app)) { + return false; + } + } + url = StringUtil.removeDom(url); + for (String image : musics) { + if (url.contains(image)) { + return true; + } + } + return false; + } + + public static String clearTag(String url) { + if (StringUtil.isEmpty(url)) { + return url; + } + if (url.startsWith("x5Play://")) { + url = StringUtils.replaceOnce(url, "x5Play://", ""); + } + String[] tagList = new String[]{ + "#ignoreVideo=true#", + "#isVideo=true#", + "#ignoreImg=true#", + "#immersiveTheme#", + "#noRecordHistory#", + "#noHistory#", + "#noLoading#", + "#isMusic=true#", + "#ignoreMusic=true#", + "#autoPage#", + "#pre#", + "#noPre#", + "#fullTheme#", + "#readTheme#", + "#gameTheme#", + "#noRefresh#", + "#background#", + "#autoCache#", + "#cacheOnly#", + "#originalSize#", + "#memoryPage#" + }; + for (String tag : tagList) { + url = StringUtils.replaceOnce(url, tag, ""); + } + return url; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountManagerCollection.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountManagerCollection.kt new file mode 100644 index 0000000..8fbfed9 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountManagerCollection.kt @@ -0,0 +1,17 @@ +package org.mozilla.xiu.browser.fxa + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import mozilla.components.service.fxa.manager.FxaAccountManager + +class AccountManagerCollection : ViewModel() { + private var _data = MutableSharedFlow(replay = 1) + val data = _data.asSharedFlow() + + fun change(fxaAccountManager: FxaAccountManager){ + _data.tryEmit(fxaAccountManager) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountProfile.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountProfile.kt new file mode 100644 index 0000000..68867b6 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountProfile.kt @@ -0,0 +1,10 @@ +package org.mozilla.xiu.browser.fxa + +import org.mozilla.xiu.browser.R + +data class AccountProfile( + val uid: String? = "00000", + val email: String? = "null@Stage.com", + val avatar: Any? = R.drawable.person_circle, + val displayName: String? = "Stage@null", +) \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountProfileViewModel.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountProfileViewModel.kt new file mode 100644 index 0000000..a905345 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountProfileViewModel.kt @@ -0,0 +1,13 @@ +package org.mozilla.xiu.browser.fxa + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class AccountProfileViewModel : ViewModel() { + private var _data = MutableStateFlow(AccountProfile()) + val data = _data.asStateFlow() + fun changeProfile(profile: AccountProfile) { + _data.value = profile + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountState.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountState.kt new file mode 100644 index 0000000..1e428ba --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountState.kt @@ -0,0 +1,8 @@ +package org.mozilla.xiu.browser.fxa + +import androidx.annotation.Keep + +@Keep +interface AccountState + + diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountStateViewModel.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountStateViewModel.kt new file mode 100644 index 0000000..2bf7e8b --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/AccountStateViewModel.kt @@ -0,0 +1,19 @@ +package org.mozilla.xiu.browser.fxa + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class AccountStateViewModel :ViewModel(){ + private val _accountStateFlow = MutableStateFlow(SyncState()) + val accountStateFlow = _accountStateFlow.asStateFlow() + + + fun sendAccountState(syncState:SyncState) { + viewModelScope.launch { + _accountStateFlow.emit(syncState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/Fxa.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/Fxa.kt new file mode 100644 index 0000000..40e83b1 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/Fxa.kt @@ -0,0 +1,238 @@ +package org.mozilla.xiu.browser.fxa + +import android.content.Context +import android.provider.Settings +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.lifecycleScope +import org.mozilla.xiu.browser.componets.popup.ReceivedTabPopupObervers +import kotlinx.coroutines.launch +import mozilla.components.browser.storage.sync.PlacesBookmarksStorage +import mozilla.components.concept.sync.* +import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient +import mozilla.components.service.fxa.* +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider +import mozilla.components.service.fxa.sync.SyncReason +import mozilla.components.service.fxa.sync.SyncStatusObserver +import mozilla.components.support.rusthttp.RustHttpConfig +import mozilla.components.support.rustlog.RustLog + + +class Fxa { + private lateinit var accountManagerCollection: AccountManagerCollection + private lateinit var accountStateViewModel: AccountStateViewModel + private lateinit var syncDevicesObserver: SyncDevicesObserver + private lateinit var fxaViewModel :AccountProfileViewModel + private lateinit var context: Context + private var syncState = SyncState() + private lateinit var mAccountManager: FxaAccountManager + + companion object { + const val CLIENT_ID = "3c49430b43dfba77" + const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" + } + var isLogin:Boolean=false + private var receivingState = false + + fun init(context: Context):FxaAccountManager{ + this.context = context + val mContext = context as LifecycleOwner + + RustLog.enable() + RustHttpConfig.setClient(lazy { HttpURLConnectionClient() }) + + fxaViewModel = ViewModelProvider(context as ViewModelStoreOwner)[AccountProfileViewModel::class.java] + accountManagerCollection = ViewModelProvider(context as ViewModelStoreOwner)[AccountManagerCollection::class.java] + accountStateViewModel = ViewModelProvider(context as ViewModelStoreOwner)[AccountStateViewModel::class.java] + syncDevicesObserver = ViewModelProvider(context as ViewModelStoreOwner)[SyncDevicesObserver::class.java] + val receivedTabPopupObervers = ViewModelProvider(context as ViewModelStoreOwner)[ReceivedTabPopupObervers::class.java] + var deviceName = Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) + + val accountManager by lazy { + FxaAccountManager( + context, + ServerConfig(Server.RELEASE, CLIENT_ID, REDIRECT_URL), + DeviceConfig( + name = "Stage on $deviceName", + type = DeviceType.MOBILE, + capabilities = setOf(DeviceCapability.SEND_TAB), + secureStateAtRest = false, + ), + SyncConfig( + setOf( + SyncEngine.Bookmarks, + SyncEngine.History + + ), + periodicSyncConfig = PeriodicSyncConfig(periodMinutes = 0, initialDelayMinutes = 1), + ), + ) + } + + mAccountManager = accountManager + + + mContext.lifecycleScope.launch{ + initState() + } + + mContext.lifecycleScope.launch { + receivedTabPopupObervers.state.collect(){ + receivingState = it + } + } + + + val bookmarksStorage = lazy { + PlacesBookmarksStorage(context) + } + GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarksStorage) + + + accountManager.register(accountObserver, owner = mContext, autoPause = true) + // Observe sync state changes. + accountManager.registerForSyncEvents(syncObserver, owner = mContext, autoPause = true) + // Observe incoming device commands. + accountManager.registerForAccountEvents(accountEventsObserver, owner = mContext, autoPause = false) + + + mContext.lifecycleScope.launch { + // Now that our account state observer is registered, we can kick off the account manager. + accountManager.start() + } + + accountManagerCollection.change(accountManager) + + + + return accountManager + + + } + + + private val deviceConstellationObserver = object : DeviceConstellationObserver { + override fun onDevicesUpdate(constellation: ConstellationState) { + syncDevicesObserver.sendAccountState(constellation.otherDevices) + } + } + + private val accountObserver = object : AccountObserver { + lateinit var lastAuthType: AuthType + override fun onLoggedOut() { + super.onLoggedOut() + isLogin = false + fxaViewModel.changeProfile( + AccountProfile() + ) + } + + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { + isLogin = true + (context as LifecycleOwner).lifecycleScope.launch { + account.deviceConstellation().registerDeviceObserver( + deviceConstellationObserver, + context as LifecycleOwner, + true, + ) + } + + (context as LifecycleOwner).lifecycleScope.launch { + mAccountManager + .authenticatedAccount() + ?.deviceConstellation() + ?.refreshDevices() + } + } + override fun onProfileUpdated(profile: Profile) { + fxaViewModel.changeProfile( + AccountProfile(profile.uid, + profile.email, + profile.avatar?.url, + profile.displayName + ) + ) + + } + override fun onFlowError(error: AuthFlowError) { + + + } + } + private val syncObserver = object : SyncStatusObserver { + override fun onError(error: Exception?) { + syncState.copy(error = true,idle = false,start = true) + accountStateViewModel.sendAccountState(syncState) + + } + + override fun onIdle() { + syncState.copy(error = false,idle = true,start = true) + accountStateViewModel.sendAccountState(syncState) + if (receivingState){ + (context as LifecycleOwner).lifecycleScope.launch { + mAccountManager.syncNow(SyncReason.User) + mAccountManager + .authenticatedAccount() + ?.deviceConstellation() + ?.pollForCommands() + + } + } + + } + + override fun onStarted() { + syncState.copy(error = false,idle = false,start = true) + accountStateViewModel.sendAccountState(syncState) + } + } + @Suppress("SetTextI18n", "NestedBlockDepth") + private val accountEventsObserver = object : AccountEventsObserver { + override fun onEvents(events: List) { + events.forEach { + when (it) { + is AccountEvent.DeviceCommandIncoming -> { + when (it.command) { + is DeviceCommandIncoming.TabReceived -> { + val cmd = it.command as DeviceCommandIncoming.TabReceived + var tabsStringified = "Tab(s) from: ${cmd.from?.displayName}\n" + cmd.entries.forEach { tab -> + tabsStringified += "${tab.title}: ${tab.url}\n" + } + android.util.Log.d("tabsStringified","get:$tabsStringified") + } + } + } + is AccountEvent.ProfileUpdated -> { + + } + is AccountEvent.AccountAuthStateChanged -> { + } + is AccountEvent.AccountDestroyed -> { + } + is AccountEvent.DeviceConnected -> { + } + is AccountEvent.DeviceDisconnected -> { + + } + is AccountEvent.Unknown ->{ + + } + } + } + } + } + + + suspend fun initState(){ + accountStateViewModel.accountStateFlow.collect(){ + syncState = it + } + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/SyncDevicesObserver.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/SyncDevicesObserver.kt new file mode 100644 index 0000000..e866b10 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/SyncDevicesObserver.kt @@ -0,0 +1,20 @@ +package org.mozilla.xiu.browser.fxa + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import mozilla.components.concept.sync.Device + +class SyncDevicesObserver:ViewModel() { + private val _syncDevicesStateFlow = MutableStateFlow>(emptyList()) + val syncDevicesStateFlow = _syncDevicesStateFlow.asStateFlow() + + + fun sendAccountState(devices: List) { + viewModelScope.launch { + _syncDevicesStateFlow.emit(devices) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/SyncLooper.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/SyncLooper.kt new file mode 100644 index 0000000..090b83e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/SyncLooper.kt @@ -0,0 +1,10 @@ +package org.mozilla.xiu.browser.fxa + +data class SyncState( + val error: Boolean = false, + val idle:Boolean = false, + val start:Boolean = false + ):AccountState + +sealed class SyncLooper { +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/TabReceivedViewModel.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/TabReceivedViewModel.kt new file mode 100644 index 0000000..edb3990 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/TabReceivedViewModel.kt @@ -0,0 +1,27 @@ +package org.mozilla.xiu.browser.fxa + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import mozilla.components.concept.sync.Device +import mozilla.components.concept.sync.DeviceType +import mozilla.components.concept.sync.TabData + +class TabReceivedViewModel: ViewModel() { + private var _tabs = MutableStateFlow>(emptyList()) + private var _device = MutableStateFlow(Device( + null.toString(), + null.toString(), + DeviceType.MOBILE, + null == true, + null, + emptyList(), + null == true, + null,)) + val tabs = _tabs.asStateFlow() + val device = _device.asStateFlow() + fun changeTabs(device: Device,tabs: List) { + _device.value = device + _tabs.value = tabs + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/UserFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/UserFragment.kt new file mode 100644 index 0000000..a0fdc27 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/UserFragment.kt @@ -0,0 +1,30 @@ +package org.mozilla.xiu.browser.fxa + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy + +class UserFragment : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + setContent { + + } + } + } + + + + + + + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/sync/BookmarkSync.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/sync/BookmarkSync.kt new file mode 100644 index 0000000..e5d2a89 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/sync/BookmarkSync.kt @@ -0,0 +1,45 @@ +package org.mozilla.xiu.browser.fxa.sync + +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.lifecycleScope +import org.mozilla.xiu.browser.fxa.AccountManagerCollection +import kotlinx.coroutines.launch +import mozilla.components.browser.storage.sync.PlacesBookmarksStorage +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider +import mozilla.components.service.fxa.sync.SyncReason + +class BookmarkSync (val context: Context) { + private var bookmarkStorage = lazy { + PlacesBookmarksStorage(context) + } + private var accountManagerCollection: AccountManagerCollection = ViewModelProvider(context as ViewModelStoreOwner)[AccountManagerCollection::class.java] + private lateinit var accountManager: FxaAccountManager + + init { + GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage) + (context as LifecycleOwner).lifecycleScope.launch { + accountManagerCollection.data.collect(){ + accountManager = it + + + } + + } + } + + fun sync(url :String,title: String){ + (context as LifecycleOwner).lifecycleScope.launch { + var a = 1 + bookmarkStorage.value.addItem("mobile______",url,title,a.toUInt()) + accountManager.syncNow(SyncReason.User) + + + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/fxa/sync/HistorySync.kt b/app/src/main/java/org/mozilla/xiu/browser/fxa/sync/HistorySync.kt new file mode 100644 index 0000000..cfcfb1d --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/fxa/sync/HistorySync.kt @@ -0,0 +1,47 @@ +package org.mozilla.xiu.browser.fxa.sync + +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.concept.storage.PageVisit +import mozilla.components.concept.storage.VisitType +import mozilla.components.service.fxa.SyncEngine +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider +import mozilla.components.service.fxa.sync.SyncReason +import org.mozilla.xiu.browser.fxa.AccountManagerCollection + +//todo 崩溃,报 undefined symbol: ffi_glean_64d5_OnGleanEvents_init_callback +class HistorySync(val context: Context) { + private var historyStorage: Lazy = lazy { + PlacesHistoryStorage(context) + } + private var accountManagerCollection: AccountManagerCollection = + ViewModelProvider(context as ViewModelStoreOwner)[AccountManagerCollection::class.java] + private lateinit var accountManager: FxaAccountManager + + init { + GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage) + (context as LifecycleOwner).lifecycleScope.launch { + accountManagerCollection.data.collect() { + accountManager = it + + + } + + } + } + + fun sync(url: String) { + (context as LifecycleOwner).lifecycleScope.launch { + historyStorage.value.recordVisit(url, PageVisit(VisitType.LINK)) + accountManager.syncNow(SyncReason.User) + + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/menu/AddonsPopupFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/menu/AddonsPopupFragment.kt new file mode 100644 index 0000000..9d2bf94 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/menu/AddonsPopupFragment.kt @@ -0,0 +1,66 @@ +package org.mozilla.xiu.browser.menu + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.databinding.FragmentAddonsBinding +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * Use the [AddonsPopupFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class AddonsPopupFragment : Fragment() { + // TODO: Rename and change types of parameters + private var param1: String? = null + private var param2: String? = null + lateinit var binding:FragmentAddonsBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding=FragmentAddonsBinding.inflate(LayoutInflater.from(context)) + var adapter= TabMenuAddonsAdapater() + binding.recyclerView3.layoutManager = LinearLayoutManager(context,RecyclerView.HORIZONTAL,false) + binding.recyclerView3.adapter=adapter + context?.let { it -> + GeckoRuntime.getDefault(it).webExtensionController.list().accept { + if(it.isNullOrEmpty()) + binding.tabAddonsView.visibility=View.GONE + else + binding.tabAddonsView.visibility=View.VISIBLE + adapter.submitList(it) + } + } + adapter.select= object : TabMenuAddonsAdapater.Select { + override fun onSelect(session: GeckoSession) { + context?.let { + GeckoRuntime.getDefault(it) }?.let { + session.open(it) + binding.tabAddonsView.setSession(session) + } + } + + } + return binding.root + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/menu/TabMenuAddonsAdapater.kt b/app/src/main/java/org/mozilla/xiu/browser/menu/TabMenuAddonsAdapater.kt new file mode 100644 index 0000000..08dc57e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/menu/TabMenuAddonsAdapater.kt @@ -0,0 +1,76 @@ +package org.mozilla.xiu.browser.menu + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.componets.MenuAddonsListCallback +import org.mozilla.xiu.browser.databinding.ItemMenuAddonsBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.WebExtension + +class TabMenuAddonsAdapater : ListAdapter(MenuAddonsListCallback) { + lateinit var select: Select + + inner class ItemTestViewHolder(private val binding: ItemMenuAddonsBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean: WebExtension, mContext: Context){ + mContext as LifecycleOwner + bean.setActionDelegate(object :WebExtension.ActionDelegate{ + override fun onBrowserAction( + extension: WebExtension, + session: GeckoSession?, + action: WebExtension.Action + ) { + mContext.lifecycleScope.launch { + binding.addonsIcon.setImageBitmap(withContext(Dispatchers.IO) { + action.icon?.getBitmap( + 128 + )?.poll() + }) + } + binding.addonsIcon.setOnClickListener { action.click() } + } + override fun onTogglePopup( + extension: WebExtension, + action: WebExtension.Action + ): GeckoResult? { + val session=GeckoSession() + select.onSelect(session) + return GeckoResult.fromValue(session) + } + override fun onOpenPopup( + extension: WebExtension, + action: WebExtension.Action + ): GeckoResult? { + val session=GeckoSession() + select.onSelect(session) + return GeckoResult.fromValue(session) + } + }) + + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemMenuAddonsBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + + } + interface Select{ + fun onSelect(session: GeckoSession) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/session/DelegateLivedata.kt b/app/src/main/java/org/mozilla/xiu/browser/session/DelegateLivedata.kt new file mode 100644 index 0000000..23c834a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/session/DelegateLivedata.kt @@ -0,0 +1,23 @@ +package org.mozilla.xiu.browser.session + +import androidx.lifecycle.LiveData + +class DelegateLivedata : LiveData() { + fun Value(sessionDelegate: SessionDelegate){ + postValue(sessionDelegate) + } + override fun onActive() { + super.onActive() + } + + override fun onInactive() { + super.onInactive() + } + companion object { + private lateinit var globalData: DelegateLivedata + fun getInstance(): DelegateLivedata { + globalData = if (Companion::globalData.isInitialized) globalData else DelegateLivedata() + return globalData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/session/GeckoViewModel.kt b/app/src/main/java/org/mozilla/xiu/browser/session/GeckoViewModel.kt new file mode 100644 index 0000000..0bd14ac --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/session/GeckoViewModel.kt @@ -0,0 +1,16 @@ +package org.mozilla.xiu.browser.session + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.mozilla.geckoview.GeckoSession + +class GeckoViewModel:ViewModel() { + private var _data = MutableStateFlow(GeckoSession()) + val data = _data.asStateFlow() + fun changeSearch(session1: GeckoSession) { + _data.value = session1 + } +} + + diff --git a/app/src/main/java/org/mozilla/xiu/browser/session/NewSession.kt b/app/src/main/java/org/mozilla/xiu/browser/session/NewSession.kt new file mode 100644 index 0000000..4d087c0 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/session/NewSession.kt @@ -0,0 +1,23 @@ +package org.mozilla.xiu.browser.session + +import android.app.Activity +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession + +fun createSession(uri: String?,activity: Activity) { + HomeLivedata.getInstance().Value(false) + val session = GeckoSession() + val sessionSettings = session.settings + val geckoViewModel= activity?.let { ViewModelProvider(it as ViewModelStoreOwner)[GeckoViewModel::class.java] }!! + SeRuSettings(sessionSettings, activity) + + activity?.let { GeckoRuntime.getDefault(it) }?.let { session.open(it) } + if (uri != null) { + session.loadUri(uri) + } + geckoViewModel.changeSearch(session) + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/session/PrivacyFlow.kt b/app/src/main/java/org/mozilla/xiu/browser/session/PrivacyFlow.kt new file mode 100644 index 0000000..9f3ee17 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/session/PrivacyFlow.kt @@ -0,0 +1,14 @@ +package org.mozilla.xiu.browser.session + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class PrivacyFlow : ViewModel() { + private var b:Boolean = false + private var _data = MutableStateFlow(b) + val data = _data.asStateFlow() + fun changeMode(b:Boolean) { + _data.value = b + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/session/ProgressEvent.kt b/app/src/main/java/org/mozilla/xiu/browser/session/ProgressEvent.kt new file mode 100644 index 0000000..8addcf1 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/session/ProgressEvent.kt @@ -0,0 +1,8 @@ +package org.mozilla.xiu.browser.session + +/** + * 作者:By 15968 + * 日期:On 2023/11/10 + * 时间:At 19:14 + */ +data class ProgressEvent(var progress: Int) diff --git a/app/src/main/java/org/mozilla/xiu/browser/session/SeRuSettings.kt b/app/src/main/java/org/mozilla/xiu/browser/session/SeRuSettings.kt new file mode 100644 index 0000000..aec00e7 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/session/SeRuSettings.kt @@ -0,0 +1,59 @@ +package org.mozilla.xiu.browser.session + +import android.app.Activity +import androidx.preference.PreferenceManager +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoRuntimeSettings +import org.mozilla.geckoview.GeckoRuntimeSettings.ALLOW_ALL +import org.mozilla.geckoview.GeckoSessionSettings +import org.mozilla.xiu.browser.utils.getSizeName + +class SeRuSettings { + private var geckoSessionSettings: GeckoSessionSettings + private lateinit var geckoRuntimeSettings: GeckoRuntimeSettings + var activity: Activity + + constructor( + geckoSessionSettings: GeckoSessionSettings, + activity: Activity + ) { + this.geckoSessionSettings = geckoSessionSettings + this.activity = activity + geckoRuntimeSettings = GeckoRuntime.getDefault(activity).settings + if (getSizeName(activity) == "large") { + geckoSessionSettings.userAgentOverride = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0" + geckoSessionSettings.viewportMode = GeckoSessionSettings.VIEWPORT_MODE_DESKTOP + geckoSessionSettings.displayMode = GeckoSessionSettings.DISPLAY_MODE_BROWSER + } else { + geckoSessionSettings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_MOBILE + + } + val sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(activity /* Activity context */) + geckoRuntimeSettings.forceUserScalableEnabled = + sharedPreferences.getBoolean("switch_userscalable", false) + geckoRuntimeSettings.automaticFontSizeAdjustment = + sharedPreferences.getBoolean("switch_automatic_fontsize", false) + geckoRuntimeSettings.aboutConfigEnabled = true + geckoRuntimeSettings.webFontsEnabled = true + geckoRuntimeSettings.loginAutofillEnabled = true + geckoRuntimeSettings.doubleTapZoomingEnabled = true + geckoRuntimeSettings.allowInsecureConnections = ALLOW_ALL + geckoRuntimeSettings.setExtensionsProcessEnabled( + sharedPreferences.getBoolean( + "switch_extension_process", + false + ) + ) + sharedPreferences.registerOnSharedPreferenceChangeListener { sharedPreferences, key -> + when (key) { + "switch_userscalable" -> geckoRuntimeSettings.forceUserScalableEnabled = + sharedPreferences.getBoolean("switch_userscalable", false) + + "switch_automatic_fontsize" -> geckoRuntimeSettings.automaticFontSizeAdjustment = + sharedPreferences.getBoolean("switch_automatic_fontsize", false) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/session/SessionDelegate.kt b/app/src/main/java/org/mozilla/xiu/browser/session/SessionDelegate.kt new file mode 100644 index 0000000..80c88b2 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/session/SessionDelegate.kt @@ -0,0 +1,602 @@ +package org.mozilla.xiu.browser.session + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import android.view.View +import android.widget.TextView +import androidx.core.graphics.drawable.toBitmap +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.google.android.material.button.MaterialButton +import com.kongzue.dialogx.dialogs.PopTip +import com.kongzue.dialogx.interfaces.OnBindView +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.mozilla.geckoview.* +import org.mozilla.geckoview.GeckoSession.ContentDelegate +import org.mozilla.geckoview.GeckoSession.NavigationDelegate +import org.mozilla.geckoview.GeckoSession.ProgressDelegate +import org.mozilla.geckoview.GeckoSession.PromptDelegate.* +import org.mozilla.xiu.browser.BR +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.componets.ContextMenuDialog +import org.mozilla.xiu.browser.componets.popup.IntentPopup +import org.mozilla.xiu.browser.database.history.History +import org.mozilla.xiu.browser.database.history.HistoryViewModel +import org.mozilla.xiu.browser.download.DownloadTask +import org.mozilla.xiu.browser.download.DownloadTaskLiveData +import org.mozilla.xiu.browser.utils.UriUtils +import org.mozilla.xiu.browser.utils.filePicker.FilePicker +import org.mozilla.xiu.browser.webextension.WebextensionSession +import java.io.IOException + + +class SessionDelegate() : BaseObservable() { + + + lateinit var session: GeckoSession + private lateinit var mContext: FragmentActivity + lateinit var login: Login + lateinit var setpic: Setpic + lateinit var pageError: PageError + val CONFIG_URL = "https://accounts.firefox.com" + + @get:Bindable + var u: String = "" + + @get:Bindable + lateinit var bitmap: Bitmap + + @get:Bindable + var mTitle: String = "" + + @get:Bindable + var active: Boolean = false + set(value) { + field = value + // 只刷新当前属性 + notifyPropertyChanged(BR.active) + } + + @get:Bindable + var privacy: Boolean = false + + @get:Bindable + var mProgress: Int = 0 + + @get:Bindable + var canBack: Boolean = false + + @get:Bindable + var canForward: Boolean = false + + @get:Bindable + var isFull: Boolean = false + + @get:Bindable + var isSecure: Boolean = false + + @get:Bindable + var secureHost: String = "" + + var oldY: Int = 0 + + @get:Bindable + var y: Int = 0 + + var downloadTasks = ArrayList() + lateinit var historyViewModel: HistoryViewModel + lateinit var intentPopup: IntentPopup + var uri: Uri? = null + private lateinit var filePicker: FilePicker + lateinit var sessionState: GeckoSession.SessionState + private lateinit var fullscreenCall: (full: Boolean) -> Unit + //private lateinit var historySync: HistorySync + + constructor( + mContext: FragmentActivity, + session: GeckoSession, + filePicker: FilePicker, + privacy: Boolean, + fullscreenCall: (full: Boolean) -> Unit + ) : this() { + this.mContext = mContext + this.session = session + this.filePicker = filePicker + this.privacy = privacy + this.fullscreenCall = fullscreenCall + notifyPropertyChanged(BR.privacy) + + + val geckoViewModel: GeckoViewModel = + ViewModelProvider(mContext).get(GeckoViewModel::class.java) + historyViewModel = ViewModelProvider(mContext).get(HistoryViewModel::class.java) + bitmap = mContext.getDrawable(R.drawable.logo72)?.toBitmap()!! + intentPopup = IntentPopup(mContext) + //historySync = HistorySync(mContext) + + + DownloadTaskLiveData.getInstance().observe(mContext) { + downloadTasks = it + } + session.contentDelegate = object : GeckoSession.ContentDelegate { + override fun onContextMenu( + session: GeckoSession, + screenX: Int, + screenY: Int, + element: GeckoSession.ContentDelegate.ContextElement + ) { + super.onContextMenu(session, screenX, screenY, element) + ContextMenuDialog(mContext, element).open() + } + + override fun onExternalResponse(session: GeckoSession, response: WebResponse) { + var uri = response.uri + val name = UriUtils.getFileName(response) + //没有下载,关闭response + try { + response.body?.close() + } catch (e: IOException) { + e.printStackTrace() + } + if (uri.endsWith("xpi")) { + WebextensionSession(mContext).install(uri) + } else { + PopTip.build() + .setCustomView(object : + OnBindView(org.mozilla.xiu.browser.R.layout.pop_mytip) { + override fun onBind(dialog: PopTip?, v: View) { + v.findViewById(R.id.textView17).text = "网页希望下载文件" + v.findViewById(R.id.materialButton7) + .setOnClickListener { + mContext.lifecycleScope.launch { + var downloadTask = DownloadTask( + mContext, + uri, + name + ) + downloadTask.open() + downloadTasks.add(downloadTask) + DownloadTaskLiveData.getInstance().Value(downloadTasks) + } + } + } + }) + .show() + + } + Log.d("ExternalResponse", uri) + + + } + + override fun onPaintStatusReset(session: GeckoSession) { + // setpic.onSetPic() + notifyPropertyChanged(BR.bitmap) + } + + override fun onFirstComposite(session: GeckoSession) { + setpic.onSetPic() + notifyPropertyChanged(BR.bitmap) + } + + override fun onTitleChange(session: GeckoSession, title: String?) { + if (title != null) { + mTitle = title + } + if (!privacy) { + var history = title?.let { History(u, it, 0) } + historyViewModel.insertHistories(history) + //historySync.sync(u) + + } + + + notifyPropertyChanged(BR.mTitle) + + } + } + session.mediaSessionDelegate = object : MediaSession.Delegate { + var orientation: Boolean? = null + override fun onFullscreen( + session: GeckoSession, + mediaSession: MediaSession, + enabled: Boolean, + meta: MediaSession.ElementMetadata? + ) { + if (!enabled) { + mContext.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER + return + } + if (meta == null) { + return + } + if (meta.width > meta.height) { + mContext.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } else { + mContext.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + } + } + session.contentDelegate = object : ContentDelegate { + override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) { + super.onFullScreen(session, fullScreen) + isFull = fullScreen + fullscreenCall(fullScreen) + notifyPropertyChanged(BR.full) + } + } + + session.progressDelegate = object : ProgressDelegate { + override fun onSecurityChange( + session: GeckoSession, + securityInfo: ProgressDelegate.SecurityInformation + ) { + super.onSecurityChange(session, securityInfo) + isSecure = securityInfo.isSecure + secureHost = securityInfo.host + "" + notifyPropertyChanged(BR.secure) + notifyPropertyChanged(BR.secureHost) + + } + + override fun onSessionStateChange( + session: GeckoSession, + sessionState: GeckoSession.SessionState + ) { + super.onSessionStateChange(session, sessionState) + this@SessionDelegate.sessionState = sessionState + } + + override fun onProgressChange(session: GeckoSession, progress: Int) { + super.onProgressChange(session, progress) + Log.d("test", "onProgressChange: $progress") + EventBus.getDefault().post(ProgressEvent(progress)) + if (progress != 100) + mProgress = progress + else mProgress = 0 + + + notifyPropertyChanged(BR.mProgress) + } + + override fun onPageStart(session: GeckoSession, url: String) { + if (url.startsWith("$CONFIG_URL/oauth/success/3c49430b43dfba77")) { + val uri = Uri.parse(url) + val code = uri.getQueryParameter("code") + val state = uri.getQueryParameter("state") + val action = uri.getQueryParameter("action") + if (code != null && state != null && action != null) { + //listener?.onLoginComplete(code, state, mContext) + //Toast.makeText(mContext,code+"**"+state,Toast.LENGTH_SHORT).show() + login.onLogin(code, state, action) + } + } + notifyPropertyChanged(BR.y) + + } + + override fun onPageStop(session: GeckoSession, success: Boolean) { + } + } + + + + session.navigationDelegate = object : NavigationDelegate { + override fun onSubframeLoadRequest( + session: GeckoSession, + request: NavigationDelegate.LoadRequest + ): GeckoResult? { + val uri = Uri.parse(request.uri) + var url = request.uri + var intent: Intent? = null; + if (uri.scheme != null) { + if (!uri.scheme!!.contains("https") && !uri.scheme!!.contains("http") && !uri.scheme!!.contains( + "about" + ) + ) { + if (url.startsWith("android-app://")) { + intent = Intent.parseUri(url, Intent.URI_ANDROID_APP_SCHEME); + } else if (url.startsWith("intent://")) { + intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); + } else { + intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME) + } + if (intent != null) { + if (intent.resolveActivity(mContext.packageManager) != null) { + PopTip.build() + .setCustomView(object : + OnBindView(org.mozilla.xiu.browser.R.layout.pop_mytip) { + override fun onBind(dialog: PopTip?, v: View) { + v.findViewById(R.id.textView17).text = + mContext.getText(R.string.intent_message) + v.findViewById(R.id.materialButton7) + .setOnClickListener { + mContext.startActivity(intent) + } + } + }) + .show() + } + } + } + Log.d("scheme2", uri.scheme!!) + + } + + return GeckoResult.allow() + } + + override fun onLoadRequest( + session: GeckoSession, + request: NavigationDelegate.LoadRequest + ): GeckoResult? { + val uri = Uri.parse(request.uri) + var url = request.uri + if (uri.scheme != null) { + if (!uri.scheme!!.contains("https") && !uri.scheme!!.contains("http") && !uri.scheme!!.contains( + "about" + ) + ) { + + var intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME) + if (intent.resolveActivity(mContext.packageManager) != null) { + PopTip.build() + .setCustomView(object : + OnBindView(org.mozilla.xiu.browser.R.layout.pop_mytip) { + override fun onBind(dialog: PopTip?, v: View) { + v.findViewById(R.id.textView17).text = + mContext.getText(R.string.intent_message) + v.findViewById(R.id.materialButton7) + .setOnClickListener { + mContext.startActivity(intent) + } + } + }) + .show() + } + + } + + } + return GeckoResult.fromValue(AllowOrDeny.ALLOW) + } + + override fun onLoadError( + session: GeckoSession, + uri: String?, + error: WebRequestError + ): GeckoResult? { + pageError.onPageError(session, uri, error) + return super.onLoadError(session, uri, error) + } + + override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) { + super.onCanGoForward(session, canGoForward) + canForward = canGoForward + notifyPropertyChanged(BR.canForward) + } + + override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) { + super.onCanGoBack(session, canGoBack) + canBack = canGoBack + notifyPropertyChanged(BR.canBack) + } + + override fun onLocationChange( + session: GeckoSession, + url: String?, + perms: MutableList + ) { + + super.onLocationChange(session, url, perms) + if (url != null) { + Log.d("可以?", url) + } + if (url != null) { + u = url + } + pageError.onPageChange() + notifyPropertyChanged(org.mozilla.xiu.browser.BR.u) + } + + override fun onNewSession( + session: GeckoSession, + uri: String + ): GeckoResult? { + val newSession = GeckoSession() + geckoViewModel.changeSearch(newSession) + return GeckoResult.fromValue(newSession) + } + } + + + /* + session.setAutofillDelegate(new Autofill.Delegate() { + @Override + public void onAutofill(@NonNull GeckoSession session, int notification, @Nullable Autofill.Node node) { + AutofillManager afm = context.getSystemService(AutofillManager.class); + if(afm!=null){ + + } + } + });*/ + + session.promptDelegate = object : GeckoSession.PromptDelegate { + override fun onAddressSave( + session: GeckoSession, + request: AutocompleteRequest + ): GeckoResult? { + Log.d("BeforeUnload", "its me") + return null + } + + override fun onSharePrompt( + session: GeckoSession, + prompt: SharePrompt + ): GeckoResult? { + Log.d("BeforeUnload", "its me") + + return null + } + + override fun onBeforeUnloadPrompt( + session: GeckoSession, + prompt: BeforeUnloadPrompt + ): GeckoResult? { + Log.d("BeforeUnload", "its me") + return null + } + + override fun onButtonPrompt( + session: GeckoSession, + prompt: ButtonPrompt + ): GeckoResult? { + val buttonDialog = org.mozilla.xiu.browser.broswer.dialog.ButtonDialog( + mContext, + prompt + ) + buttonDialog.showDialog() + Log.d("ButtonPrompt", "its me") + + return GeckoResult.fromValue(buttonDialog.dialogResult) + } + + override fun onPopupPrompt( + session: GeckoSession, + prompt: PopupPrompt + ): GeckoResult? { + Log.d("Popup", "its me") + + return null + } + + override fun onAuthPrompt( + session: GeckoSession, + prompt: AuthPrompt + ): GeckoResult? { + Log.d("AuthPrompt", "its me") + + return null + } + + override fun onTextPrompt( + session: GeckoSession, + prompt: TextPrompt + ): GeckoResult? { + val alertDialog = + org.mozilla.xiu.browser.broswer.dialog.TextDialog(mContext, prompt) + alertDialog.showDialog() + Log.d("TextPrompt", "its me") + + return GeckoResult.fromValue(alertDialog.dialogResult) + + } + + override fun onRepostConfirmPrompt( + session: GeckoSession, + prompt: RepostConfirmPrompt + ): GeckoResult? { + val confirmPrompt = + org.mozilla.xiu.browser.broswer.dialog.ConfirmDialog( + mContext, + prompt + ) + confirmPrompt.showDialog() + Log.d("RepostConfirm", "its me") + + return GeckoResult.fromValue(confirmPrompt.dialogResult) + } + + override fun onFilePrompt( + session: GeckoSession, + prompt: FilePrompt + ): GeckoResult? { + val getFile = org.mozilla.xiu.browser.utils.filePicker.GetFile(mContext, filePicker) + getFile.open(mContext, prompt.mimeTypes) + Log.d("onFilePrompt", getFile.uri.toString()) + return GeckoResult.fromValue(prompt.confirm(mContext, getFile.uri)) + } + + + override fun onChoicePrompt( + session: GeckoSession, + prompt: ChoicePrompt + ): GeckoResult? { + //prompt. + val jsChoiceDialog = + org.mozilla.xiu.browser.broswer.dialog.JsChoiceDialog( + mContext, + prompt + ) + jsChoiceDialog.showDialog() + Log.d("ButtonPrompt", "its me") + + return GeckoResult.fromValue(prompt.confirm(jsChoiceDialog.dialogResult.toString())) + } + + override fun onAlertPrompt( + session: GeckoSession, + prompt: AlertPrompt + ): GeckoResult? { + val alertDialog = + org.mozilla.xiu.browser.broswer.dialog.AlertDialog(mContext, prompt) + alertDialog.showDialog() + Log.d("ButtonPrompt", "its me") + + return GeckoResult.fromValue(alertDialog.dialogResult) + } + } + + } + + fun close() { + session.close() + bitmap.recycle() + } + + fun open() { + if (!session.isOpen) + session.open(GeckoRuntime.getDefault(mContext)) + } + + @SuppressLint("SuspiciousIndentation") + fun resume() { + if (!session.isOpen) + session.open(GeckoRuntime.getDefault(mContext)) + session.restoreState(sessionState!!) + } + + interface Login { + fun onLogin(code: String, state: String, action: String) + } + + interface Setpic { + fun onSetPic() + } + + interface PageError { + fun onPageError( + session: GeckoSession, + uri: String?, + error: WebRequestError + ) + + fun onPageChange() + } + + +} + + + + + + diff --git a/app/src/main/java/org/mozilla/xiu/browser/session/SessionViewModel.kt b/app/src/main/java/org/mozilla/xiu/browser/session/SessionViewModel.kt new file mode 100644 index 0000000..f8544eb --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/session/SessionViewModel.kt @@ -0,0 +1,10 @@ +package org.mozilla.xiu.browser.session + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class SessionViewModel() : ViewModel() { + val currentSession: MutableLiveData by lazy { + MutableLiveData() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/xiu/browser/settings/SettingsFragment.kt new file mode 100644 index 0000000..bea1aa4 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/settings/SettingsFragment.kt @@ -0,0 +1,41 @@ +package org.mozilla.xiu.browser.settings + +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import org.mozilla.xiu.browser.MainActivity +import org.mozilla.xiu.browser.R +import org.mozilla.xiu.browser.session.createSession + +/** + * 2023.2.11 19:10 + * 正月廿一 + * thallo + **/ +class SettingsFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.root_preferences, rootKey) + findPreference("settingAbout")?.setOnPreferenceClickListener { + findNavController().navigate(R.id.action_settingsFragment_to_aboutFragment) + false + } + findPreference("searching")?.setOnPreferenceClickListener { + findNavController().navigate(R.id.action_settingsFragment_to_settingsSearching2) + false + } + findPreference("addons")?.setOnPreferenceClickListener { + findNavController().navigate(R.id.action_settingsFragment_to_addonsManagerFragment) + false + } + findPreference("privacyAndService")?.setOnPreferenceClickListener { + findNavController().navigate(R.id.action_settingsFragment_to_privacyAndServiceFragment) + false + } + preferenceScreen.onPreferenceClickListener = Preference.OnPreferenceClickListener { true } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/settings/SettingsSearching.kt b/app/src/main/java/org/mozilla/xiu/browser/settings/SettingsSearching.kt new file mode 100644 index 0000000..d20f43d --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/settings/SettingsSearching.kt @@ -0,0 +1,12 @@ +package org.mozilla.xiu.browser.settings + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import org.mozilla.xiu.browser.R + +class SettingsSearching : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.searching_preferences, rootKey) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/tab/AddTabLiveData.kt b/app/src/main/java/org/mozilla/xiu/browser/tab/AddTabLiveData.kt new file mode 100644 index 0000000..e4c69b6 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/tab/AddTabLiveData.kt @@ -0,0 +1,23 @@ +package org.mozilla.xiu.browser.tab + +import androidx.lifecycle.LiveData + +class AddTabLiveData : LiveData() { + fun Value(i: Int){ + postValue(i) + } + override fun onActive() { + super.onActive() + } + + override fun onInactive() { + super.onInactive() + } + companion object { + private lateinit var globalData: AddTabLiveData + fun getInstance(): AddTabLiveData { + globalData = if (Companion::globalData.isInitialized) globalData else AddTabLiveData() + return globalData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/tab/DelegateListLiveData.kt b/app/src/main/java/org/mozilla/xiu/browser/tab/DelegateListLiveData.kt new file mode 100644 index 0000000..f001ec6 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/tab/DelegateListLiveData.kt @@ -0,0 +1,24 @@ +package org.mozilla.xiu.browser.tab + +import androidx.lifecycle.LiveData +import org.mozilla.xiu.browser.session.SessionDelegate + +class DelegateListLiveData : LiveData>() { + fun Value(sessionDelegates:ArrayList){ + postValue(sessionDelegates) + } + override fun onActive() { + super.onActive() + } + + override fun onInactive() { + super.onInactive() + } + companion object { + private lateinit var globalData: DelegateListLiveData + fun getInstance(): DelegateListLiveData { + globalData = if (Companion::globalData.isInitialized) globalData else DelegateListLiveData() + return globalData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/tab/RemoveTabLiveData.kt b/app/src/main/java/org/mozilla/xiu/browser/tab/RemoveTabLiveData.kt new file mode 100644 index 0000000..063df27 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/tab/RemoveTabLiveData.kt @@ -0,0 +1,23 @@ +package org.mozilla.xiu.browser.tab + +import androidx.lifecycle.LiveData + +class RemoveTabLiveData : LiveData() { + fun Value(int: Int){ + postValue(int) + } + override fun onActive() { + super.onActive() + } + + override fun onInactive() { + super.onInactive() + } + companion object { + private lateinit var globalData: RemoveTabLiveData + fun getInstance(): RemoveTabLiveData { + globalData = if (Companion::globalData.isInitialized) globalData else RemoveTabLiveData() + return globalData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/tab/TabListAdapter.kt b/app/src/main/java/org/mozilla/xiu/browser/tab/TabListAdapter.kt new file mode 100644 index 0000000..b25e637 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/tab/TabListAdapter.kt @@ -0,0 +1,49 @@ +package org.mozilla.xiu.browser.tab + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.databinding.ItemTablistPhoneBinding +import org.mozilla.xiu.browser.session.DelegateLivedata +import org.mozilla.xiu.browser.session.SessionDelegate + +class TabListAdapter : ListAdapter(TabListCallback) { + lateinit var select:Select + + inner class ItemTestViewHolder(private val binding: ItemTablistPhoneBinding): RecyclerView.ViewHolder(binding.root){ + fun bind(bean:SessionDelegate,mContext: Context){ + binding.user=getItem(adapterPosition) + binding.wholeTab?.setOnClickListener{ + DelegateLivedata.getInstance().Value(getItem(adapterPosition)) + HomeLivedata.getInstance().Value(false) + select.onSelect() + } + binding.materialButton?.setOnClickListener { RemoveTabLiveData.getInstance().Value(adapterPosition) + } + binding.deleteButton?.setOnClickListener { RemoveTabLiveData.getInstance().Value(adapterPosition) + } + + + + } + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { + return ItemTestViewHolder(ItemTablistPhoneBinding.inflate(LayoutInflater.from(parent.context),parent,false)) + } + + override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { + //通过ListAdapter内部实现的getItem方法找到对应的Bean + holder.bind(getItem(holder.adapterPosition),holder.itemView.context) + holder.itemView.context + + } + interface Select{ + fun onSelect() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/tab/TabListCallback.kt b/app/src/main/java/org/mozilla/xiu/browser/tab/TabListCallback.kt new file mode 100644 index 0000000..b6f9692 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/tab/TabListCallback.kt @@ -0,0 +1,23 @@ +package org.mozilla.xiu.browser.tab + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import org.mozilla.xiu.browser.session.SessionDelegate + +object TabListCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SessionDelegate, newItem: SessionDelegate): Boolean { + return oldItem.u == newItem.u + && oldItem.session == newItem.session + && oldItem.mTitle == newItem.mTitle + && oldItem.bitmap == newItem.bitmap + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: SessionDelegate, newItem: SessionDelegate): Boolean { + return oldItem.u == newItem.u + && oldItem.session == newItem.session + && oldItem.mTitle == newItem.mTitle + && oldItem.bitmap == newItem.bitmap + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/AndroidBarUtils.java b/app/src/main/java/org/mozilla/xiu/browser/utils/AndroidBarUtils.java new file mode 100644 index 0000000..f78a843 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/AndroidBarUtils.java @@ -0,0 +1,311 @@ +package org.mozilla.xiu.browser.utils; + +/** + * 作者:By 15968 + * 日期:On 2021/7/14 + * 时间:At 15:45 + */ + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Color; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.drawerlayout.widget.DrawerLayout; + +import org.mozilla.xiu.browser.R; + +import java.lang.reflect.Field; + +/** + * @author zsl + * @date 2018/6/13 + * @description StatusBar 和 NavigationBar 的工具类 + */ +public class AndroidBarUtils { + + private static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height"; + private static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height"; + private static final String NAV_BAR_WIDTH_RES_NAME = "navigation_bar_width"; + public static int mainFlag = -10000; + private static int defPadding; + + /** + * 判断当前环境是否 Dark Mode + */ + public static boolean isDarkMode(Context context) { + return (context.getResources().getConfiguration().uiMode & + Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } + /** + * 设置透明StatusBar,默认文字为白色 + * + * @param activity Activity + */ + public static void setTranslucent(Activity activity) { + setTranslucentStatusBar(activity, true); + } + + /** + * 设置 DrawerLayout 在4.4版本下透明,不然会出现白边 + * + * @param drawerLayout DrawerLayout + */ + public static void setTranslucentDrawerLayout(DrawerLayout drawerLayout) { + if (drawerLayout != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + drawerLayout.setFitsSystemWindows(true); + drawerLayout.setClipToPadding(false); + } + } + + /** + * 设置透明StatusBar + * + * @param activity Activity + */ + public static void setTranslucentStatusBar(Activity activity, boolean trans) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + Window window = activity.getWindow(); + //透明状态栏 +// if (trans) { +// window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); +// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); +// } else { +// window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); +// window.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); +// } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (trans) { + if (-10000 == mainFlag) { + mainFlag = window.getDecorView().getSystemUiVisibility(); + } + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + window.setStatusBarColor(Color.TRANSPARENT); + } else { + window.getDecorView().setSystemUiVisibility(mainFlag); + int c = activity.getResources().getColor(R.color.white); + StatusBarCompatUtil.setStatusBarColor(activity, c); + } + //5.0及以上版本 +// createNavBar(activity); + } else { + //4.4版本 + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + + /** + * 设置透明StatusBar + * + * @param activity Activity + */ + public static void setTranslucentStatusBar2(Activity activity, boolean isInit) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + Window window = activity.getWindow(); + //透明状态栏 + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + int uiConfig = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + if (isInit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (isDarkMode(activity)) { + uiConfig = uiConfig & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } else { + uiConfig = uiConfig | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } + } + } + window.getDecorView().setSystemUiVisibility(uiConfig); + window.setStatusBarColor(Color.TRANSPARENT); + window.setNavigationBarColor(Color.TRANSPARENT); + + //5.0及以上版本 +// createNavBar(activity); + } else { + //4.4版本 + // TODO 这里不知道要不要处理,没有 4.4 的测试机,遇到问题在解开看看吧 + // window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + + /** + * 设置透明StatusBar + * + * @param activity Activity + */ + public static void setTranslucentStatusBar3(Activity activity, boolean isInit) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return; + } + Window window = activity.getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + int uiConfig = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + if (isInit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (isDarkMode(activity)) { + uiConfig = uiConfig & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } else { + uiConfig = uiConfig | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } + } + } + window.getDecorView().setSystemUiVisibility(uiConfig); + window.setStatusBarColor(Color.TRANSPARENT); + } + /** + * Android 6.0使用原始的主题适配 + * + * @param activity Activity + * @param darkMode 是否是黑色模式 + */ + public static void setBarDarkMode(Activity activity, boolean darkMode) { + Window window = activity.getWindow(); + if (window == null) { + return; + } + //设置StatusBar模式 + if (darkMode) {//黑色模式 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//设置statusBar和navigationBar为黑色 + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } else {//设置statusBar为黑色 + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } + } + } else {//白色模式 + int statusBarFlag = View.SYSTEM_UI_FLAG_VISIBLE; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + statusBarFlag = window.getDecorView().getSystemUiVisibility() + & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//设置statusBar为白色,navigationBar为灰色 +// int navBarFlag = window.getDecorView().getSystemUiVisibility() +// & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;//如果想让navigationBar为白色,那么就使用这个代码。 + int navBarFlag = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + window.getDecorView().setSystemUiVisibility(navBarFlag | statusBarFlag); + } else { + window.getDecorView().setSystemUiVisibility(statusBarFlag); + } + } + setHuaWeiStatusBar(darkMode, window); + } + + /** + * 设置华为手机 StatusBar + * + * @param darkMode 是否是黑色模式 + * @param window window + */ + private static void setHuaWeiStatusBar(boolean darkMode, Window window) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + Class decorViewClazz = Class.forName("com.android.internal.policy.DecorView"); + Field field = decorViewClazz.getDeclaredField("mSemiTransparentStatusBarColor"); + field.setAccessible(darkMode); + field.setInt(window.getDecorView(), Color.TRANSPARENT); //改为透明 + } catch (ClassNotFoundException e) { + Log.e("setHuaWeiStatusBar", "HuaWei status bar 模式设置失败"); + } catch (IllegalAccessException e) { + Log.e("setHuaWeiStatusBar", "HuaWei status bar 模式设置失败"); + } catch (NoSuchFieldException e) { + Log.e("setHuaWeiStatusBar", "HuaWei status bar 模式设置失败"); + } + } + } + + /** + * 获取状态栏高度 + * + * @param context context + * @return 状态栏高度 + */ + public static int getStatusBarHeight(Activity context) { + // 获得状态栏高度 + return MyStatusBarUtil.getStatusBarHeight(context); + } + + /** + * 获取Bar高度 + * + * @param context context + * @param barName 名称 + * @return Bar高度 + */ + public static int getBarHeight(Context context, String barName) { + // 获得状态栏高度 + int resourceId = context.getResources().getIdentifier(barName, "dimen", "android"); + return context.getResources().getDimensionPixelSize(resourceId); + } + + public static void isNavigationBarExist(@NonNull Activity activity, final OnNavigationStateListener onNavigationStateListener) { + final int height = getBarHeight(activity, NAV_BAR_WIDTH_RES_NAME); + activity.getWindow().getDecorView().setOnApplyWindowInsetsListener((v, windowInsets) -> { + boolean isShowing = false; + int b = 0; + if (windowInsets != null) { + b = windowInsets.getSystemWindowInsetBottom(); + isShowing = (b == height); + } + if (onNavigationStateListener != null && b <= height) { + onNavigationStateListener.onNavigationState(isShowing, b); + } + return windowInsets; + }); + } + + public interface OnNavigationStateListener { + void onNavigationState(boolean isShow, int height); + } + + /** + * 设置BarPaddingTop + * + * @param context Activity + * @param view View[ToolBar、TitleBar、navigationView.getHeaderView(0)] + */ + public static void setBarPaddingTop(Activity context, View view) { + int paddingStart = view.getPaddingStart(); + int paddingEnd = view.getPaddingEnd(); + int paddingBottom = view.getPaddingBottom(); + int statusBarHeight = getStatusBarHeight(context); + //改变titleBar的高度 + ViewGroup.LayoutParams lp = view.getLayoutParams(); + lp.height += statusBarHeight; + view.setLayoutParams(lp); + //设置paddingTop + view.setPaddingRelative(paddingStart, statusBarHeight, paddingEnd, paddingBottom); + } + + /** + * 设置BarPaddingTop + * + * @param context Activity + * @param view View[ToolBar、TitleBar、navigationView.getHeaderView(0)] + */ + public static void setBarPaddingTopForFrameLayout(Activity context, View view) { + int statusBarHeight = getStatusBarHeight(context); + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) view.getLayoutParams(); + layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin + statusBarHeight, layoutParams.rightMargin, layoutParams.bottomMargin); + view.setLayoutParams(layoutParams); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/ClipboardUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/ClipboardUtil.java new file mode 100644 index 0000000..8ad1ade --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/ClipboardUtil.java @@ -0,0 +1,203 @@ +package org.mozilla.xiu.browser.utils; + +import static android.content.Context.CLIPBOARD_SERVICE; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ClipboardManager; +import android.content.Context; +import android.text.TextUtils; +import android.view.View; +import android.widget.EditText; + +/** + * 作者:By hdy + * 日期:On 2017/11/5 + * 时间:At 8:59 + */ + +public class ClipboardUtil { + private static final String TAG = "ClipboardUtil"; + + /** + * 获取剪贴板 + * + * @param context + * @return + */ + public static void getText(Context context, View v, ClipListener clipListener) { + getText(context, v, clipListener, 1000); + } + + /** + * 获取剪贴板 + * + * @param context + * @return + */ + public static void getText(Context context, View v, ClipListener clipListener, long delay) { + if (android.os.Build.VERSION.SDK_INT >= 29) { + v.postDelayed(() -> { + EditText et = new EditText(context); + et.requestFocus(); + et.setOnClickListener(view -> clipListener.hasText(getTextNow(context))); + et.performClick(); + }, delay); + } else { + clipListener.hasText(getTextNow(context)); + } + } + + /** + * 获取剪贴板 + * + * @param context + * @return + */ + public static void getTextNoDelay(Context context, ClipListener clipListener) { + if (android.os.Build.VERSION.SDK_INT >= 29) { + EditText et = new EditText(context); + et.requestFocus(); + et.setOnClickListener(view -> clipListener.hasText(getTextNow(context))); + et.performClick(); + } else { + clipListener.hasText(getTextNow(context)); + } + } + + /** + * 获取剪贴板 + * + * @param context + * @return + */ + public static String getTextNow(Context context) { + String text = ""; + try { + text = tryGetTextNow(context); + } catch (Exception e) { + e.printStackTrace(); + } +// Log.d(TAG, "getTextNow: "+ text); + return text; + } + + public static String tryGetTextNow(Context context) { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); + //无数据时直接返回 + if (clipboard != null && !clipboard.hasPrimaryClip()) { + return ""; + } + //如果是文本信息 +// Log.d(TAG, "tryGetTextNow: " + JSON.toJSONString(clipboard == null ? "" : clipboard.getPrimaryClipDescription().getMimeType(0))); + if (clipboard != null && clipboard.getPrimaryClipDescription() != null && (clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) + || clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML))) { + ClipData cdText = clipboard.getPrimaryClip(); +// Log.d(TAG, "tryGetTextNow: "+ JSON.toJSONString(cdText.getItemAt(0))); + ClipData.Item item = null; + if (cdText != null) { + item = cdText.getItemAt(0); + } + //此处是TEXT文本信息 + if (item != null) { + if (item.getText() == null) { + return ""; + } else { + return item.getText().toString(); + } + } + } + return ""; + } + + public static boolean copyToClipboard(Context context, String str) { + return copyToClipboard(context, str, true); + } + + public static boolean copyToClipboard(Context context, String str, boolean showToast) { + if(str == null){ + return false; + } + if ("".equals(str)) { + //EventBus.getDefault().post(new ClearDetectedEvent()); + } + if (android.os.Build.VERSION.SDK_INT >= 29) { + EditText et = new EditText(context); + et.requestFocus(); + final String s = str; + et.setOnClickListener(view -> { + String str1 = s; + ClipboardManager clipManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (str1.length() > 10240) { + str1 = str1.substring(0, 10240); + } + ClipData mClipData = ClipData.newPlainText("Label", str1); + if (clipManager != null) { + clipManager.setPrimaryClip(mClipData); + if (!TextUtils.isEmpty(str1) && showToast) { + ToastMgr.shortBottomCenter(context, "复制成功"); + } + } + }); + et.performClick(); + return true; + } + ClipboardManager clipManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (str.length() > 1024) { + str = str.substring(0, 10240); + } + ClipData mClipData = ClipData.newPlainText("Label", str); + if (clipManager != null) { + clipManager.setPrimaryClip(mClipData); + if (!TextUtils.isEmpty(str) && showToast) { + ToastMgr.shortBottomCenter(context, "复制成功"); + } + } + return true; + } + + public static boolean copyToClipboardForce(Context context, String str) { + return copyToClipboardForce(context, str, true); + } + + public static boolean copyToClipboardForce(Context context, String str, boolean showToast) { + if(str == null){ + return false; + } + if ("".equals(str)) { + //EventBus.getDefault().post(new ClearDetectedEvent()); + } + if (android.os.Build.VERSION.SDK_INT >= 29) { + EditText et = new EditText(context); + et.requestFocus(); + final String s = str; + et.setOnClickListener(view -> { + ClipboardManager clipManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData mClipData = ClipData.newPlainText("Label", s); + if (clipManager != null) { + clipManager.setPrimaryClip(mClipData); + if (!TextUtils.isEmpty(s) && showToast) { + ToastMgr.shortBottomCenter(context, "复制成功"); + } + } + }); + et.performClick(); + return true; + } + ClipboardManager clipManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData mClipData = ClipData.newPlainText("Label", str); + if (clipManager != null) { + clipManager.setPrimaryClip(mClipData); + if (!TextUtils.isEmpty(str) && showToast) { + ToastMgr.shortBottomCenter(context, "复制成功"); + } + } + return true; + } + + public interface ClipListener { + void hasText(String text); + } +} + + diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/CollectionUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/CollectionUtil.java new file mode 100644 index 0000000..533ea47 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/CollectionUtil.java @@ -0,0 +1,61 @@ +package org.mozilla.xiu.browser.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * 作者:By hdy + * 日期:On 2019/4/3 + * 时间:At 12:17 + */ +public class CollectionUtil { + public static List asList(T... a) { + List data = new ArrayList<>(); + Collections.addAll(data, a); + return data; + } + + public static String[] toStrArray(List list) { + if (list == null) { + return new String[]{}; + } + String[] d = new String[list.size()]; + for (int i = 0; i < list.size(); i++) { + d[i] = list.get(i); + } + return d; + } + + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + + public static boolean isEmpty(String[] collection) { + return (collection == null || collection.length <= 0); + } + + public static boolean isNotEmpty(Collection collection) { + return !isEmpty(collection); + } + + public static String listToString(List list, String cha) { + StringBuilder builder = new StringBuilder(); + if (list == null || list.size() <= 0) { + return ""; + } else if (list.size() <= 1) { + return list.get(0); + } else { + builder.append(list.get(0)); + } + for (int i = 1; i < list.size(); i++) { + builder.append(cha).append(list.get(i)); + } + return builder.toString(); + } + + public static String listToString(List list) { + return listToString(list, "&&"); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/CommonUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/CommonUtil.java new file mode 100644 index 0000000..e81f470 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/CommonUtil.java @@ -0,0 +1,32 @@ +package org.mozilla.xiu.browser.utils; + +import android.content.Context; +import android.content.pm.PackageManager; + +/** + * 作者:By 15968 + * 日期:On 2020/2/7 + * 时间:At 18:22 + */ +public class CommonUtil { + public static String getVersionName(Context context) { + try { + String pkName = context.getPackageName(); + return context.getPackageManager().getPackageInfo( + pkName, 0).versionName; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return ""; + } + } + public static int getVersionCode(Context context) { + try { + String pkName = context.getPackageName(); + return context.getPackageManager().getPackageInfo( + pkName, 0).versionCode; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return -1; + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/DisplayUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/DisplayUtil.java new file mode 100644 index 0000000..4eacd58 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/DisplayUtil.java @@ -0,0 +1,48 @@ +package org.mozilla.xiu.browser.utils; + +import android.content.Context; + +/** + * 作者:By 15968 + * 日期:On 2019/9/28 + * 时间:At 17:17 + */ +public class DisplayUtil { + public static int pxToDp(Context context, int px) { + if (context == null) { + return px; + } + if (px <= 0) { + return px; + } + float scale = context.getResources().getDisplayMetrics().density; + return (int) (px / scale + 0.5f); + } + + public static int dpToPx(Context context, int dp) { + if (context == null) { + return dp; + } + if (dp <= 0) { + return dp; + } + float scale = context.getResources().getDisplayMetrics().density; + return (int) (dp * scale + 0.5f); + } + + /** + * 根据分辨率从 dp 的单位 转成为 px(像素) + */ + public static int dp2px(Context context, float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * 根据分辨率从 px(像素) 的单位 转成为 dp + */ + public static int px2dp(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/FileUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/FileUtil.java new file mode 100644 index 0000000..19500f3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/FileUtil.java @@ -0,0 +1,817 @@ +package org.mozilla.xiu.browser.utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.widget.Toast; + +import org.mozilla.xiu.browser.App; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.URL; +import java.nio.file.Files; +import java.text.DecimalFormat; +import java.util.regex.Pattern; + +/** + * @author fisher + * @description 文件工具类 + */ + +public class FileUtil { + + public static String png2Ts(String pngFile) { + // 1、 获取相同目录的ts名称 + int prefixIndex = pngFile.lastIndexOf("."); + String targetName = pngFile.substring(0, prefixIndex) + ".ts"; + File f = new File(targetName); + if (f.exists()) { + f.delete(); + } + try (RandomAccessFile sourceRandomAccessFile = new RandomAccessFile(pngFile, "r"); + RandomAccessFile targetRandomAccessFile = new RandomAccessFile(targetName, "rw")) { + sourceRandomAccessFile.seek(0); + // 跳过png的头 + sourceRandomAccessFile.skipBytes(8); + byte[] buffer = new byte[1024 * 1024]; + int len; + while ((len = sourceRandomAccessFile.read(buffer)) != -1) { + targetRandomAccessFile.write(buffer, 0, len); + } + } catch (Exception e) { + e.printStackTrace(); + } + return targetName; + } + + public static int getFileCount(String dir) { + File file = new File(dir); + if (!file.exists()) { + return 0; + } + if (file.isFile()) { + return 1; + } else { + int count = 0; + File[] files = file.listFiles(); + if (files != null && files.length > 0) { + for (File file1 : files) { + count = count + getFileCount(file1.getAbsolutePath()); + } + } + return count; + } + } + + //private static final Log Debug = LogFactory.getLog(FileUtil.class); + + // 获取从classpath根目录开始读取文件注意转化成中文 + public static String getCPFile(String path) { + URL url = FileUtil.class.getClassLoader().getResource(path); + String filepath = url.getFile(); + File file = new File(filepath); + byte[] retBuffer = new byte[(int) file.length()]; + try { + FileInputStream fis = new FileInputStream(filepath); + fis.read(retBuffer); + fis.close(); + return new String(retBuffer, "GBK"); + } catch (IOException e) { + //Debug.error("FilesInAppUtil.getCPFile读取文件异常:" + e.toString()); + return null; + } + } + + + /** + * 利用java本地拷贝文件及文件夹,如何实现文件夹对文件夹的拷贝呢?如果文件夹里还有文件夹怎么办呢? + * + * @param objDir 目标文件夹 + * @param srcDir 源的文件夹 + */ + public static void copyDirectiory(final String objDir, final String srcDir) + throws IOException { + (new File(objDir)).mkdirs(); + File[] file = (new File(srcDir)).listFiles(); + if (file == null) { + return; + } + for (File value : file) { + if (value.isFile()) { + FileInputStream input = new FileInputStream(value); + FileOutputStream output = new FileOutputStream(objDir + "/" + + value.getName()); + byte[] b = new byte[1024 * 5]; + int len; + while ((len = input.read(b)) != -1) { + output.write(b, 0, len); + } + output.flush(); + output.close(); + input.close(); + } + if (value.isDirectory()) { + copyDirectiory(objDir + "/" + value.getName(), srcDir + "/" + + value.getName()); + } + } + + } + + /** + * 将一个文件inName拷贝到另外一个文件outName中 + * + * @param inName 源文件路径 + * @param outName 目标文件路径 + */ + public static void copyFile(final String inName, final String outName) + throws FileNotFoundException, IOException { + copy(new File(inName), new File(outName)); + } + + /** + * Copy a file from an opened InputStream to opened OutputStream + * + * @param is source InputStream + * @param os target OutputStream + * @param close 写入之后是否需要关闭OutputStream + */ + public static void copyFile(InputStream is, OutputStream os, boolean close) + throws IOException { + int b; + while ((b = is.read()) != -1) { + os.write(b); + } + is.close(); + if (close) + os.close(); + } + + public static void copyFile(Reader is, Writer os, boolean close) + throws IOException { + int b; + while ((b = is.read()) != -1) { + os.write(b); + } + is.close(); + if (close) + os.close(); + } + + public static void copyFile(String inName, PrintWriter pw, boolean close) + throws FileNotFoundException, IOException { + BufferedReader is = new BufferedReader(new FileReader(inName)); + copyFile(is, pw, close); + } + + + /** + * 复制文件 + * + * @param source 输入文件 + * @param target 输出文件 + */ + public static void copy(File source, File target) { + if (source.getAbsolutePath().equals(target.getAbsolutePath())) { + return; + } + File dir = target.getParentFile(); + if (dir != null && !dir.exists()) { + dir.mkdirs(); + } + FileInputStream fileInputStream = null; + FileOutputStream fileOutputStream = null; + try { + fileInputStream = new FileInputStream(source); + fileOutputStream = new FileOutputStream(target); + int len; + byte[] buffer = new byte[1024]; + while ((len = fileInputStream.read(buffer)) > 0) { + fileOutputStream.write(buffer, 0, len); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + if (fileOutputStream != null) { + fileOutputStream.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + + /** + * 从文件inName中读取第一行的内容 + * + * @param inName 源文件路径 + * @return 第一行的内容 + */ + public static String readLine(String inName) throws FileNotFoundException, + IOException { + BufferedReader is = new BufferedReader(new FileReader(inName)); + String line = null; + line = is.readLine(); + is.close(); + return line; + } + + /** + * default buffer size + */ + private static final int BLKSIZ = 8192; + + public static void copyFileBuffered(String inName, String outName) + throws FileNotFoundException, IOException { + InputStream is = new FileInputStream(inName); + OutputStream os = new FileOutputStream(outName); + int count = 0; + byte b[] = new byte[BLKSIZ]; + while ((count = is.read(b)) != -1) { + os.write(b, 0, count); + } + is.close(); + os.close(); + } + + /** + * 将String变成文本文件 + * + * @param text 源String + * @param fileName 目标文件路径 + */ + public static void stringToFile(final String text, final String fileName) + throws IOException { + File file = new File(fileName); + File dir = file.getParentFile(); + if (dir != null && !dir.exists()) { + dir.mkdirs(); + } + try (BufferedWriter os = new BufferedWriter(new FileWriter(fileName))) { + os.write(text); + os.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 打开文件获得BufferedReader + * + * @param fileName 目标文件路径 + * @return BufferedReader + */ + public static BufferedReader openFile(String fileName) throws IOException { + return new BufferedReader(new FileReader(fileName)); + } + + /** + * 获取文件filePath的字节编码byte[] + * + * @param filePath 文件全路径 + * @return 文件内容的字节编码 + * @roseuid 3FBE26DE027D + */ + public static byte[] fileToBytes(String filePath) { + if (filePath == null) { + //Debug.info("路径为空:"); + return null; + } + + File tmpFile = new File(filePath); + + byte[] retBuffer = new byte[(int) tmpFile.length()]; + try (FileInputStream fis = new FileInputStream(filePath)) { + fis.read(retBuffer); + return retBuffer; + } catch (IOException e) { + //Debug.error("读取文件异常:" + e.toString()); + return null; + } + } + + public static InputStream fileToInputStream(String filePath) { + if (filePath == null) { + //Debug.info("路径为空:"); + return null; + } + try { + return new FileInputStream(filePath); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 将byte[]转化成文件fullFilePath + * + * @param fullFilePath 文件全路径 + * @param content 源byte[] + */ + public static void bytesToFile(final String fullFilePath, final byte[] content) { + if (fullFilePath == null || content == null) { + return; + } + + // 创建相应的目录 + File f = new File(getDir(fullFilePath)); + if (!f.exists()) { + f.mkdirs(); + } + + try (FileOutputStream fos = new FileOutputStream(fullFilePath)) { + fos.write(content); + fos.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + /** + * 根据传入的文件全路径,返回文件所在路径 + * + * @param fullPath 文件全路径 + * @return 文件所在路径 + */ + public static String getDir(String fullPath) { + int iPos1 = fullPath.lastIndexOf("/"); + int iPos2 = fullPath.lastIndexOf("\\"); + iPos1 = (iPos1 > iPos2 ? iPos1 : iPos2); + return fullPath.substring(0, iPos1 + 1); + } + + public static void saveFile(Context context, Runnable task) { + com.lxj.xpopup.util.XPermission.create(context, com.lxj.xpopup.util.PermissionConstants.STORAGE) + .callback(new com.lxj.xpopup.util.XPermission.SimpleCallback() { + @Override + public void onGranted() { + task.run(); + } + + @Override + public void onDenied() { + Toast.makeText(context, "请授予文件存储权限!", Toast.LENGTH_SHORT).show(); + } + }).request(); + } + + /** + * 根据传入的文件全路径,返回文件全名(包括后缀名) + * + * @param fullPath 文件全路径 + * @return 文件全名(包括后缀名) + */ + public static String getFileName(String fullPath) { + fullPath = fullPath.replace("file://", ""); + File file = new File(fullPath); + if (file.exists()) { + return file.getName(); + } else { + file = new File(decodeUrl(fullPath, "UTF-8")); + if (file.exists()) { + return file.getName(); + } + } + int iPos1 = fullPath.lastIndexOf("/"); + int iPos2 = fullPath.lastIndexOf("\\"); + iPos1 = (iPos1 > iPos2 ? iPos1 : iPos2); + return fullPath.substring(iPos1 + 1); + } + + public static String getFilePath(String fullPath) { + fullPath = fullPath.replace("file://", ""); + File file = new File(fullPath); + if (file.exists()) { + return file.getAbsolutePath(); + } else { + file = new File(decodeUrl(fullPath, "UTF-8")); + if (file.exists()) { + return file.getAbsolutePath(); + } + } + return null; + } + + /** + * 获得文件名fileName中的后缀名 + * + * @param fileName 源文件名 + * @return String 后缀名 + */ + public static String getFileSuffix(String fileName) { + return fileName.substring(fileName.lastIndexOf(".") + 1, + fileName.length()); + } + + /** + * 根据传入的文件全名(包括后缀名)或者文件全路径返回文件名(没有后缀名) + * + * @param fullPath 文件全名(包括后缀名)或者文件全路径 + * @return 文件名(没有后缀名) + */ + public static String getPureFileName(String fullPath) { + String fileFullName = getFileName(fullPath); + return fileFullName.substring(0, fileFullName.lastIndexOf(".")); + } + + /** + * 转换文件路径中的\\为/ + * + * @param filePath 要转换的文件路径 + * @return String + */ + public static String wrapFilePath(String filePath) { + filePath.replace('\\', '/'); + if (filePath.charAt(filePath.length() - 1) != '/') { + filePath += "/"; + } + return filePath; + } + + /** + * 删除整个目录path,包括该目录下所有的子目录和文件 + * + * @param path + */ + public static void deleteDirs(final String path) { + File rootFile = new File(path); + if (!rootFile.exists()) { + return; + } + if (!rootFile.isDirectory()) { + rootFile.delete(); + return; + } + File[] files = rootFile.listFiles(); + if (files == null) { + rootFile.delete(); + return; + } + for (int i = 0; i < files.length; i++) { + File file = files[i]; + if (file.isDirectory()) { + deleteDirs(file.getPath()); + } else { + file.delete(); + } + } + rootFile.delete(); + } + + /** + * 删除整个目录path,包括该目录下所有的子目录和文件 + * + * @param path + */ + public static void deleteFile(final String path) { + File rootFile = new File(path); + if (rootFile.exists()) { + if (rootFile.isDirectory()) { + deleteDirs(path); + return; + } + rootFile.delete(); + } + } + + /** */ + /** + * 文件重命名 + * + * @param path 文件目录 + * @param oldname 原来的文件名 + * @param newname 新文件名 + */ + public static void renameFile(String path, String oldname, String newname) { + if (!oldname.equals(newname)) {//新的文件名和以前文件名不同时,才有必要进行重命名 + File oldfile = new File(path + File.separator + oldname); + File newfile = new File(path + File.separator + newname); + if (!oldfile.exists()) { + return;//重命名文件不存在 + } + if (newfile.exists())//若在该目录下已经有一个文件和新文件名相同,则不允许重命名 + System.out.println(newname + "已经存在!"); + else { + oldfile.renameTo(newfile); + } + } else { + System.out.println("新文件名和旧文件名相同..."); + } + } + + + /** */ + /** + * 文件夹重命名 + */ + public static boolean renameDir(String oldDirPath, String newDirPath) { + File oleFile = new File(oldDirPath); //要重命名的文件或文件夹 + File newFile = new File(newDirPath); //重命名为zhidian1 + return oleFile.renameTo(newFile); //执行重命名 + } + + public static String fileToString(String filePath) { + StringBuilder buffer = new StringBuilder(); + try (BufferedReader os = new BufferedReader(new FileReader(filePath))) { + String valueString; + int count = -1; + while ((valueString = os.readLine()) != null) { + if (count == -1) { + count++; + buffer.append(valueString); + } else { + buffer.append("\n").append(valueString); + } + } + } catch (IOException e) { + e.printStackTrace(); + //Timber.d(e, "文件异常%s", e.getMessage()); + } + return buffer.toString(); + } + + public static String getFormatedFileSize(long size) { + if (size < 1024) { + return new DecimalFormat("#").format(size) + "B"; + } else if (size < 1048576) { + return new DecimalFormat("#").format((double) size / 1024) + "KB"; + } else if (size < 1073741824) { + return new DecimalFormat("#.00").format((double) size / 1048576) + "MB"; + } else { + return new DecimalFormat("#.00").format((double) size / 1073741824) + "GB"; + } + } + + public static String getResourceName(String url) { + if (url == null) { + return null; + } + url = url.split("\\?")[0].split("##")[0]; + String[] s = url.split("/"); + url = s[s.length - 1]; + if (url.lastIndexOf(".") < 1) { + return null; + } + if (url.contains(".")) { + return decodeUrl(url, "UTF-8"); + } + return null; + } + + public static String getExtension(String fileName) { + if (fileName == null) { + return ""; + } + fileName = fileName.split("\\?")[0].split("##")[0]; + String[] s = fileName.split("/"); + if (s.length <= 0) { + return ""; + } + fileName = s[s.length - 1]; + if (fileName.lastIndexOf(".") < 1) { + return ""; + } + return fileName.substring(fileName.lastIndexOf(".") + 1); + } + + + public static String getSimpleName(String fileName) { + if (fileName == null) { + return fileName; + } + int index = fileName.lastIndexOf("."); + if (index < 1) { + return fileName; + } else { + return fileName.substring(0, index); + } + } + + private static String decodeUrl(String str, String code) {//url解码 + try { + str = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25"); + str = str.replaceAll("\\+", "%2B"); + str = java.net.URLDecoder.decode(str, code); + } catch (UnsupportedEncodingException e) { + } + return str; + } + + public static String getNameByUrl(String url) { + String name = getFileName0(url); + return decodeUrl(name, "UTF-8"); + } + + /** + * 根据传入的文件全路径,返回文件全名(包括后缀名) + * + * @param fullPath 文件全路径 + * @return 文件全名(包括后缀名) + */ + public static String getFileName0(String fullPath) { + int iPos1 = fullPath.lastIndexOf("/"); + int iPos2 = fullPath.lastIndexOf("\\"); + iPos1 = (iPos1 > iPos2 ? iPos1 : iPos2); + return fullPath.substring(iPos1 + 1); + } + + public static String getName(String fileName) { + if (fileName.lastIndexOf(".") < 1) { + return fileName; + } + return fileName.substring(0, fileName.lastIndexOf(".")); + } + + public static long getFolderSize(File file) { + if (!file.isDirectory()) { + return file.length(); + } + long size = 0; + File[] fileList = file.listFiles(); + for (File aFileList : fileList) { + if (aFileList.isDirectory()) { + size = size + getFolderSize(aFileList); + } else { + size = size + aFileList.length(); + } + } + return size; + } + + public static String fileNameFilter(String fileName) { + if (fileName == null) { + return ""; + } + fileName = fileName.replace(" ", "-"); + Pattern FilePattern = Pattern.compile("[\\\\/:*?\"<>|.]"); + return FilePattern.matcher(fileName).replaceAll("_"); + } + + + /** + * 生成视频播放网页 + */ + public static void generateHtml(String fileDir, String fileName) { + String h1 = "\n" + + "\n" + + "\n" + + " \n" + + " 方圆影视播放页\n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + ""; + String html = h1 + fileName + h2; + try { + stringToFile(html, fileDir + File.separator + "index.html"); + } catch (IOException e) { + e.printStackTrace(); + } + copyFilesFromAssets(App.application, "player", fileDir); + } + + public static void copyFilesFromAssets(Context context, String assetsPath, String savePath) { + try { + String[] fileNames = context.getAssets().list(assetsPath);// 获取assets目录下的所有文件及目录名 + if (fileNames.length > 0) {// 如果是目录 + File file = new File(savePath); + file.mkdirs();// 如果文件夹不存在,则递归 + for (String fileName : fileNames) { + copyFilesFromAssets(context, assetsPath + "/" + fileName, + savePath + "/" + fileName); + } + } else {// 如果是文件 + InputStream is = context.getAssets().open(assetsPath); + FileOutputStream fos = new FileOutputStream(new File(savePath)); + byte[] buffer = new byte[1024]; + int byteCount = 0; + while ((byteCount = is.read(buffer)) != -1) {// 循环从输入流读取 + // buffer字节 + fos.write(buffer, 0, byteCount);// 将读取的输入流写入到输出流 + } + fos.flush();// 刷新缓冲区 + is.close(); + fos.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void makeSureDirExist(String path) { + File file = new File(path); + if (!file.exists()) { + File dir = file.getParentFile(); + if (dir != null) { + dir.mkdirs(); + } + } + } + + public static String moveFileCompat(File oldFile, File newFile) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + Files.move(oldFile.toPath(), newFile.toPath()); + return newFile.getPath(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + boolean isCopySuccess = oldFile.renameTo(newFile); + if (isCopySuccess) { + return newFile.getPath(); + } + } + return null; + } + + public static byte[] toBytes(InputStream inputStream) { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + write(inputStream, output); + //因为是给JS用,他们大概率不会主动关闭,因此这里直接关闭流 + inputStream.close(); + return output.toByteArray(); + } catch (Exception e) { + return new byte[0]; + } + } + + public static InputStream toInputStream(byte[] bytes) { + return new ByteArrayInputStream(bytes); + } + + public static void write(InputStream inputStream, OutputStream outputStream) throws IOException { + int len; + byte[] buffer = new byte[4096]; + while ((len = inputStream.read(buffer)) != -1) outputStream.write(buffer, 0, len); + } + + public static void bitmapToFile(Bitmap bitmap, File file) throws IOException { + try (FileOutputStream out = new FileOutputStream(file)) { + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); + out.flush(); + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/FilesInAppUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/FilesInAppUtil.java new file mode 100644 index 0000000..b1a5036 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/FilesInAppUtil.java @@ -0,0 +1,206 @@ +package org.mozilla.xiu.browser.utils; + +/** + * 作者:By hdy + * 日期:On 2017/11/9 + * 时间:At 14:00 + */ + +import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; + +import androidx.core.content.FileProvider; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; + +public class FilesInAppUtil { + + + public static Bitmap getImageFromAssetsFile(Context context, String fileName) { + Bitmap image = null; + AssetManager am = context.getResources().getAssets(); + try { + InputStream is = am.open(fileName); + image = BitmapFactory.decodeStream(is); + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return image; + } + + public static void copyAssets(Context context, String assetDir, String dir) { + String[] files; + try { + files = context.getResources().getAssets().list(assetDir); + } catch (IOException e1) { + return; + } + File mWorkingPath = new File(dir); + // if this directory does not exists, make one. + if (!mWorkingPath.exists()) { + if (!mWorkingPath.mkdirs()) { + + } + } + + if (files != null) { + for (int i = 0; i < files.length; i++) { + try { + String fileName = files[i]; + // we make sure file name not contains '.' to be a folder. + if (!fileName.contains(".")) { + if (0 == assetDir.length()) { + copyAssets(context, fileName, dir + fileName + "/"); + } else { + copyAssets(context, assetDir + "/" + fileName, dir + fileName + "/"); + } + continue; + } + File outFile = new File(mWorkingPath, fileName); + if (outFile.exists()) + outFile.delete(); + InputStream in = null; + if (0 != assetDir.length()) + in = context.getAssets().open(assetDir + "/" + fileName); + else + in = context.getAssets().open(fileName); + OutputStream out = new FileOutputStream(outFile); + + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + + in.close(); + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * 快速读取程序应用包下的文件内容 + * + * @param context 上下文 + * @param filename 文件名称 + * @return 文件内容 + * @throws IOException + */ + public static String read(Context context, String filename) + throws IOException { + File file = context.getFileStreamPath(filename); + if (file == null || !file.exists()) { + return ""; + } + FileInputStream inStream = context.openFileInput(filename); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + while ((len = inStream.read(buffer)) != -1) { + outStream.write(buffer, 0, len); + } + byte[] data = outStream.toByteArray(); + return new String(data); + } + + /** + * 写入应用程序包files目录下文件 + * + * @param context 上下文 + * @param fileName 文件名称 + * @param content 文件内容 + */ + public static void writeEnd(Context context, String fileName, String content) { + try { + FileOutputStream outStream = context.openFileOutput(fileName, + Context.MODE_APPEND);//表示如果已存在则追加数据 + outStream.write(content.getBytes()); + outStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 写入应用程序包files目录下文件 + * + * @param context 上下文 + * @param fileName 文件名称 + * @param content 文件内容 + */ + public static void write(Context context, String fileName, String content) { + try { + FileOutputStream outStream = context.openFileOutput(fileName, + Context.MODE_PRIVATE); + outStream.write(content.getBytes()); + outStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static String getFilePath(Context context, String fileName) { + return context.getFilesDir() + File.separator + fileName; + } + + public static boolean exist(Context context, String fileName) { + File file = new File(context.getFilesDir() + File.separator + fileName); + return file.exists(); + } + + public static String getAssetsString(Context context, String fileName) { + //将json数据变成字符串 + StringBuilder stringBuilder = new StringBuilder(); + try { + //获取assets资源管理器 + AssetManager assetManager = context.getAssets(); + //通过管理器打开文件并读取 + BufferedReader bf = new BufferedReader(new InputStreamReader( + assetManager.open(fileName))); + String line; + int count = -1; + while ((line = bf.readLine()) != null) { + if (count == -1) { + count++; + stringBuilder.append(line); + } else { + stringBuilder.append("\n").append(line); + } + } + } catch (Throwable e) { + e.printStackTrace(); + } + return stringBuilder.toString(); + } + + + public static Uri getUri(Context context, String url) { + if (url.startsWith("file:") || url.startsWith("content") || url.startsWith("/")) { + try { + return FileProvider.getUriForFile(context, "org.mozilla.xiu.browser.provider", new File(url.replaceFirst("file://", ""))); + } catch (Exception e) { + e.printStackTrace(); + return Uri.parse(url); + } + } else { + return Uri.parse(url); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/FullScreenUtils.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/FullScreenUtils.kt new file mode 100644 index 0000000..17b7481 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/FullScreenUtils.kt @@ -0,0 +1,30 @@ +package org.mozilla.xiu.browser.utils + +import android.app.Activity +import android.view.View +import android.view.WindowManager + + +/**@RequiresApi(Build.VERSION_CODES.R) +fun ScreenUtils(context: Activity) { + context.window.insetsController?.hide(WindowInsets.Type.statusBars()) + context.window.insetsController?.hide(WindowInsets.Type.navigationBars()) + +}**/ +fun FullScreen(context: Activity) +{ + context.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + val decorView: View = context.getWindow().getDecorView() + val uiOptions: Int = + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN + decorView.setSystemUiVisibility(uiOptions) + decorView.setOnSystemUiVisibilityChangeListener(object : + View.OnSystemUiVisibilityChangeListener { + override fun onSystemUiVisibilityChange(i: Int) { + if (i and View.SYSTEM_UI_FLAG_FULLSCREEN === 0) { + decorView.setSystemUiVisibility(uiOptions) + } else { + } + } + }) +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/GroupUtils.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/GroupUtils.kt new file mode 100644 index 0000000..63a7c1b --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/GroupUtils.kt @@ -0,0 +1,46 @@ +package org.mozilla.xiu.browser.utils + +import org.mozilla.xiu.browser.database.bookmark.Bookmark +import org.mozilla.xiu.browser.database.history.History + +class GroupUtils{ + var list:List + private lateinit var list2:List + private var mList= ArrayList() + private var group: String? = null + var tags= ArrayList() + + constructor(list: List) { + this.list = list + if(!list.isNullOrEmpty()) + retrieval() + + } + + fun groupBookmark():List{ + return mList.toList() + } + fun groupTagBookmark():ArrayList{ + return tags + } + fun groupHistory():List{ + return list2 + } + + fun retrieval(){ + group= list[0]?.file + group?.let { tags.add(it) } + for (i in list) + { + if (i != null) { + if (i.file==group) + mList.add(i) + if (list.lastIndexOf(i)==list.size-1){ + list=list.toMutableList().apply { removeAll(mList) } + if (list.isNotEmpty()) + retrieval() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/MyStatusBarUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/MyStatusBarUtil.java new file mode 100644 index 0000000..67ad044 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/MyStatusBarUtil.java @@ -0,0 +1,202 @@ +package org.mozilla.xiu.browser.utils; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.WindowManager; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * 作者:By 15968 + * 日期:On 2019/10/4 + * 时间:At 13:23 + */ +public class MyStatusBarUtil { + /** + * 设置状态栏颜色 + * + * @param activity 需要设置的 activity + * @param color 状态栏颜色值 + */ + public static void setColor(Activity activity, @ColorInt int color) { + StatusBarCompatUtil.setStatusBarColor(activity, color); + } + + /** + * 设置状态栏颜色 + * + * @param activity 需要设置的 activity + * @param color 状态栏颜色值 + */ + public static void setColorNoTranslucent(Activity activity, @ColorInt int color) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + StatusBarCompatUtil.setStatusBarColor(activity, color); + return; + } + StatusBarCompatUtil.setStatusBarColor(activity, color); + } + + public static boolean isDark(int color) { + int red = (color & 0xff0000) >> 16; + int green = (color & 0x00ff00) >> 8; + int blue = (color & 0x0000ff); + return isDark((double) red, (double) green, (double) blue); + } + + public static boolean isDark(Double r, Double g, Double b) { + return !(r * 0.299 + g * 0.578 + b * 0.114 >= 192); + } + + public static int getStatusBarHeight(Context context) { + Class c = null; + Object obj = null; + Field field = null; + int x = 0, sbar = 0; + try { + c = Class.forName("com.android.internal.R$dimen"); + obj = c.newInstance(); + field = c.getField("status_bar_height"); + x = Integer.parseInt(field.get(obj).toString()); + sbar = context.getResources().getDimensionPixelSize(x); + return sbar; + } catch (Exception e) { + e.printStackTrace(); + } + int result = DisplayUtil.dpToPx(context, 20); + try { + int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + } catch (Resources.NotFoundException e) { + e.printStackTrace(); + } + return result; + } + + public static int getNavigationBarHeight(Context context) { + int resourceId; + int rid = context.getResources().getIdentifier("config_showNavigationBar", "bool", "android"); + if (rid != 0) { + resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + return context.getResources().getDimensionPixelSize(resourceId); + } else { + return 0; + } + } + + //获取是否存在NavigationBar + public static boolean checkDeviceHasNavigationBar(Context context) { + boolean hasNavigationBar = false; + Resources rs = context.getResources(); + int id = rs.getIdentifier("config_showNavigationBar", "bool", "android"); + if (id > 0) { + hasNavigationBar = rs.getBoolean(id); + } + try { + Class systemPropertiesClass = Class.forName("android.os.SystemProperties"); + Method m = systemPropertiesClass.getMethod("get", String.class); + String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys"); + if ("1".equals(navBarOverride)) { + hasNavigationBar = false; + } else if ("0".equals(navBarOverride)) { + hasNavigationBar = true; + } + } catch (Exception e) { + + } + return hasNavigationBar; + + } + + public static int getVirtualBarHeight(Context context) { + int vh = 0; + if (!checkDeviceHasNavigationBar(context)) { + return vh; + } + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = null; + if (windowManager != null) { + display = windowManager.getDefaultDisplay(); + } + DisplayMetrics dm = new DisplayMetrics(); + try { + @SuppressWarnings("rawtypes") + Class c = Class.forName("android.view.Display"); + @SuppressWarnings("unchecked") + Method method = c.getMethod("getRealMetrics", DisplayMetrics.class); + method.invoke(display, dm); + vh = dm.heightPixels - display.getHeight(); + } catch (Exception e) { + e.printStackTrace(); + } + return vh; + } + + /** + * 判断虚拟导航栏是否显示 + * + * @param context 上下文对象 + * @return true(显示虚拟导航栏),false(不显示或不支持虚拟导航栏) + */ + public static boolean checkNavigationBarShow(@NonNull Activity context) { + Display display = context.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + Point realSize = new Point(); + display.getSize(size); + display.getRealSize(realSize); + return realSize.y != size.y; + } + + /** + * 获取 虚拟按键的高度 + * + * @param context + * @return + */ + public static int getBottomStatusHeight(Activity context) { + if (checkNavigationBarShow(context)) { + int totalHeight = getDpi(context); + int contentHeight = getScreenHeight(context); + return totalHeight - contentHeight; + } else { + return 0; + } + } + + //获取屏幕原始尺寸高度,包括虚拟功能键高度 + public static int getDpi(Context context) { + int dpi = 0; + WindowManager windowManager = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + Display display = windowManager.getDefaultDisplay(); + DisplayMetrics displayMetrics = new DisplayMetrics(); + @SuppressWarnings("rawtypes") + Class c; + try { + c = Class.forName("android.view.Display"); + @SuppressWarnings("unchecked") + Method method = c.getMethod("getRealMetrics", DisplayMetrics.class); + method.invoke(display, displayMetrics); + dpi = displayMetrics.heightPixels; + } catch (Exception e) { + e.printStackTrace(); + } + return dpi; + } + + //获取屏幕高度 不包含虚拟按键= + public static int getScreenHeight(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.heightPixels; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/PreferenceConstant.java b/app/src/main/java/org/mozilla/xiu/browser/utils/PreferenceConstant.java new file mode 100644 index 0000000..b0ac92d --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/PreferenceConstant.java @@ -0,0 +1,51 @@ +package org.mozilla.xiu.browser.utils; + +/** + * 作者:By 15968 + * 日期:On 2020/8/1 + * 时间:At 12:43 + */ + +public class PreferenceConstant { + /** + * 文件,key + */ + public static final String FILE_NORMAL = "share_data"; + public static final String KEY_thirdPlayerNotGoDefault = "thirdPlayerNotGoDefault"; + public static final String KEY_multipleDeviceSyncMode = "multipleDeviceSyncMode"; + public static final String KEY_multipleDeviceSyncSilence = "multipleDeviceSyncSilence"; + public static final String KEY_deviceName = "deviceName"; + public static final String KEY_excludeSyncGroup = "excludeSyncGroup"; + public static final String KEY_oneScreen = "oneScreen"; + public static final String KEY_adBlock = "adBlock"; + public static final String KEY_useNotch = "useNotch"; + public static final String KEY_homeName = "homeName"; + + + /** + * 文件,key + */ + public static final String FILE_SETTING_CONFIG = "setting_config"; + + /** + * 文件,key + */ + public static final String FILE_WEB_DAV = "web_dav"; + public static final String KEY_webDavUrl = "webDavUrl"; + public static final String KEY_webDavAccount = "webDavAccount"; + public static final String KEY_webDavPassword = "webDavPassword"; + public static final String KEY_webDavFilePrefix = "webDavFilePrefix"; + public static final String KEY_webDavBackTime = "webDavBackTime"; + public static final String KEY_webDavLastBackup = "webDavLastBackup"; + public static final String KEY_DIRECT_FULLSCREEN = "directFullScreen"; + public static final String KEY_DIRECT_FULLSCREEN_MODE = "directFullScreenMode"; + public static final String KEY_DIRECT_BACK = "directBack"; + + /** + * 小窗模式设置 + * + * 文件,key + */ + public static final String KEY_BACK_TO_PIP = "backToPIP"; + public static final String KEY_BACKGROUND_TO_PIP = "backgroundToPIP"; +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/PreferenceMgr.java b/app/src/main/java/org/mozilla/xiu/browser/utils/PreferenceMgr.java new file mode 100644 index 0000000..2a56466 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/PreferenceMgr.java @@ -0,0 +1,312 @@ +package org.mozilla.xiu.browser.utils; + +/** + * 作者:By hdy + * 日期:On 2017/11/6 + * 时间:At 17:01 + */ + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.util.Base64; +import android.widget.ImageView; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 主要功能:用于存储缓存数据 + * + * @Prject: CommonUtilLibrary + * @Package: com.jingewenku.abrahamcaijin.commonutil + * @author: AbrahamCaiJin + * @date: 2017年05月04日 14:13 + * @Copyright: 个人版权所有 + * @Company: + * @version: 1.0.0 + */ +public class PreferenceMgr { + /** + * 保存在手机里面的文件名 + */ + private static String FILE_NAME = "share_data"; + public static String SETTING_CONFIG = "setting_config"; + + public static Map all(Context context, String fileName) { + SharedPreferences sp = context.getSharedPreferences(fileName, Context.MODE_PRIVATE); + return sp.getAll(); + } + + /** + * 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法 + */ + public static void put(Context context, String key, Object object) { + put(context, FILE_NAME, key, object); + } + + public static void put(Context context, String fileName, String key, Object object) { + + SharedPreferences sp = context.getSharedPreferences(fileName, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + + if (object instanceof String) { + editor.putString(key, (String) object); + } else if (object instanceof Integer) { + editor.putInt(key, (Integer) object); + } else if (object instanceof Boolean) { + editor.putBoolean(key, (Boolean) object); + } else if (object instanceof Float) { + editor.putFloat(key, (Float) object); + } else if (object instanceof Long) { + editor.putLong(key, (Long) object); + } else if (object instanceof Collection) { + editor.putStringSet(key, new HashSet((Collection) object)); + } else { + editor.putString(key, object.toString()); + } + SharedPreferencesCompat.apply(editor); + } + + /** + * 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值 + */ + public static Object get(Context context, String key, Object defaultObject) { + return get(context, FILE_NAME, key, defaultObject); + } + + public static Object get(Context context, String fileName, String key, Object defaultObject) { + SharedPreferences sp = context.getSharedPreferences(fileName, + Context.MODE_PRIVATE); + if (defaultObject instanceof String) { + return sp.getString(key, (String) defaultObject); + } else if (defaultObject instanceof Integer) { + return sp.getInt(key, (Integer) defaultObject); + } else if (defaultObject instanceof Boolean) { + return sp.getBoolean(key, (Boolean) defaultObject); + } else if (defaultObject instanceof Float) { + return sp.getFloat(key, (Float) defaultObject); + } else if (defaultObject instanceof Long) { + return sp.getLong(key, (Long) defaultObject); + } else if (defaultObject instanceof Set) { + return sp.getStringSet(key, (Set) defaultObject); + } + return null; + } + + public static String getString(Context context, String key, String defaultObject) { + return getString(context, FILE_NAME, key, defaultObject); + } + + public static String getString(Context context, String fileName, String key, String defaultObject) { + return context.getSharedPreferences(fileName, Context.MODE_PRIVATE).getString(key, defaultObject); + } + + public static int getInt(Context context, String key, int defaultObject) { + try { + return getInt(context, FILE_NAME, key, defaultObject); + } catch (ClassCastException e) { + return defaultObject; + } + } + + public static int getInt(Context context, String fileName, String key, int defaultObject) { + try { + return context.getSharedPreferences(fileName, Context.MODE_PRIVATE).getInt(key, defaultObject); + } catch (ClassCastException e) { + return defaultObject; + } + } + + public static boolean getBoolean(Context context, String key, boolean defaultObject) { + return getBoolean(context, FILE_NAME, key, defaultObject); + } + + public static boolean getBoolean(Context context, String fileName, String key, boolean defaultObject) { + return context.getSharedPreferences(fileName, Context.MODE_PRIVATE).getBoolean(key, defaultObject); + } + + public static float getFloat(Context context, String key, float defaultObject) { + try { + return getFloat(context, FILE_NAME, key, defaultObject); + } catch (ClassCastException e) { + float real = getInt(context, FILE_NAME, key, (int) defaultObject) + 0f; + put(context, FILE_NAME, key, real); + return real; + } + } + + public static float getFloat(Context context, String fileName, String key, float defaultObject) { + try { + return context.getSharedPreferences(fileName, Context.MODE_PRIVATE).getFloat(key, defaultObject); + } catch (ClassCastException e) { + float real = getInt(context, fileName, key, (int) defaultObject) + 0f; + put(context, fileName, key, real); + return real; + } + } + + public static long getLong(Context context, String key, long defaultObject) { + try { + return getLong(context, FILE_NAME, key, defaultObject); + } catch (ClassCastException e) { + long real = (long) getInt(context, FILE_NAME, key, (int) defaultObject); + put(context, FILE_NAME, key, real); + return real; + } + } + + public static long getLong(Context context, String fileName, String key, long defaultObject) { + try { + return context.getSharedPreferences(fileName, Context.MODE_PRIVATE).getLong(key, defaultObject); + } catch (ClassCastException e) { + long real = (long) getInt(context, fileName, key, (int) defaultObject); + put(context, fileName, key, real); + return real; + } + } + + /** + * 移除某个key值已经对应的值 + */ + public static void remove(Context context, String key) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.remove(key); + SharedPreferencesCompat.apply(editor); + } + + /** + * 移除某个key值已经对应的值 + */ + public static void remove(Context context, String file, String key) { + SharedPreferences sp = context.getSharedPreferences(file, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.remove(key); + SharedPreferencesCompat.apply(editor); + } + + /** + * 清除所有数据 + */ + public static void clear(Context context) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.clear(); + SharedPreferencesCompat.apply(editor); + } + + /** + * 查询某个key是否已经存在 + */ + public static boolean contains(Context context, String key) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.contains(key); + } + + /** + * 查询某个key是否已经存在 + */ + public static boolean contains(Context context, String file, String key) { + SharedPreferences sp = context.getSharedPreferences(file, + Context.MODE_PRIVATE); + return sp.contains(key); + } + + /** + * 返回所有的键值对 + */ + public static Map getAll(Context context) { + SharedPreferences sp = context.getSharedPreferences(FILE_NAME, + Context.MODE_PRIVATE); + return sp.getAll(); + } + + + /** + * 保存图片到SharedPreferences + * + * @param mContext + * @param imageView + */ + public static void putImage(Context mContext, String key, ImageView imageView) { + BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable(); + Bitmap bitmap = drawable.getBitmap(); + // 将Bitmap压缩成字节数组输出流 + ByteArrayOutputStream byStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 80, byStream); + // 利用Base64将我们的字节数组输出流转换成String + byte[] byteArray = byStream.toByteArray(); + String imgString = new String(Base64.encodeToString(byteArray, Base64.NO_WRAP)); + // 将String保存shareUtils + PreferenceMgr.put(mContext, key, imgString); + } + + /** + * 从SharedPreferences读取图片 + * + * @param mContext + * @param imageView + */ + public static Bitmap getImage(Context mContext, String key, ImageView imageView) { + String imgString = (String) PreferenceMgr.get(mContext, key, ""); + if (!imgString.equals("")) { + // 利用Base64将我们string转换 + byte[] byteArray = Base64.decode(imgString, Base64.NO_WRAP); + ByteArrayInputStream byStream = new ByteArrayInputStream(byteArray); + // 生成bitmap + return BitmapFactory.decodeStream(byStream); + } + return null; + } + + /** + * 创建一个解决SharedPreferencesCompat.apply方法的一个兼容类 + */ + private static class SharedPreferencesCompat { + private static final Method sApplyMethod = findApplyMethod(); + + /** + * 反射查找apply的方法 + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Method findApplyMethod() { + try { + Class clz = SharedPreferences.Editor.class; + return clz.getMethod("apply"); + } catch (NoSuchMethodException e) { + } + + return null; + } + + /** + * 如果找到则使用apply执行,否则使用commit + */ + public static void apply(SharedPreferences.Editor editor) { + try { + if (sApplyMethod != null) { + sApplyMethod.invoke(editor); + return; + } + } catch (IllegalArgumentException e) { + } catch (IllegalAccessException e) { + } catch (InvocationTargetException e) { + } + editor.commit(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/RoundedCornersTransform.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/RoundedCornersTransform.kt new file mode 100644 index 0000000..61fff86 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/RoundedCornersTransform.kt @@ -0,0 +1,129 @@ +package org.mozilla.xiu.browser.utils + +import android.content.Context +import android.graphics.* +import com.bumptech.glide.Glide +import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapResource +import java.security.MessageDigest + +//Koltin版本 +class RoundedCornersTransform( + context: Context?, + var radius: Float, + var leftTop: Boolean = true, + var rightTop: Boolean = true, + var leftBottom: Boolean = true, + var rightBottom: Boolean = true +) : + Transformation { + private val mBitmapPool: BitmapPool = Glide.get(context!!).bitmapPool + + private val id = javaClass.name + private val idBytes = id.toByteArray(Charsets.UTF_8); + + override fun transform( + context: Context, + resource: Resource, + outWidth: Int, + outHeight: Int + ): Resource { + val source: Bitmap = resource.get() + var finalWidth: Int + var finalHeight: Int + //输出目标的宽高或高宽比例 + var scale: Float + if (outWidth > outHeight) { + //如果 输出宽度 > 输出高度 求高宽比 + scale = outHeight.toFloat() / outWidth.toFloat() + finalWidth = source.width + //固定原图宽度,求最终高度 + finalHeight = (source.width.toFloat() * scale).toInt() + if (finalHeight > source.height) { + //如果 求出的最终高度 > 原图高度 求宽高比 + scale = outWidth.toFloat() / outHeight.toFloat() + finalHeight = source.height + //固定原图高度,求最终宽度 + finalWidth = (source.height.toFloat() * scale).toInt() + } + } else if (outWidth < outHeight) { + //如果 输出宽度 < 输出高度 求宽高比 + scale = outWidth.toFloat() / outHeight.toFloat() + finalHeight = source.height + //固定原图高度,求最终宽度 + finalWidth = (source.height.toFloat() * scale).toInt() + if (finalWidth > source.width) { + //如果 求出的最终宽度 > 原图宽度 求高宽比 + scale = outHeight.toFloat() / outWidth.toFloat() + finalWidth = source.width + finalHeight = (source.width.toFloat() * scale).toInt() + } + } else { + //如果 输出宽度=输出高度 + finalHeight = source.height + finalWidth = finalHeight + } + + //修正圆角 + radius *= finalHeight.toFloat() / outHeight.toFloat() + val outBitmap: Bitmap = mBitmapPool[finalWidth, finalHeight, Bitmap.Config.ARGB_8888] + val canvas = Canvas(outBitmap) + val paint = Paint() + //关联画笔绘制的原图bitmap + val shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + //计算中心位置,进行偏移 + val width: Int = (source.width - finalWidth) / 2 + val height: Int = (source.height - finalHeight) / 2 + if (width != 0 || height != 0) { + val matrix = Matrix() + matrix.setTranslate((-width).toFloat(), (-height).toFloat()) + shader.setLocalMatrix(matrix) + } + paint.shader = shader + paint.isAntiAlias = true + val rectF = RectF(0.0f, 0.0f, canvas.width.toFloat(), canvas.height.toFloat()) + //先绘制圆角矩形 + canvas.drawRoundRect(rectF, radius, radius, paint) + + //左上角圆角 + if (!leftTop) { + canvas.drawRect(0f, 0f, radius, radius, paint) + } + //右上角圆角 + if (!rightTop) { + canvas.drawRect(canvas.width - radius, 0f, canvas.width.toFloat(), radius, paint) + } + //左下角圆角 + if (!leftBottom) { + canvas.drawRect(0f, canvas.height - radius, radius, canvas.height.toFloat(), paint) + } + //右下角圆角 + if (!rightBottom) { + canvas.drawRect( + canvas.width - radius, + canvas.height - radius, + canvas.width.toFloat(), + canvas.height.toFloat(), + paint + ) + } + return BitmapResource.obtain(outBitmap, mBitmapPool)!! + } + + /** must override */ + override fun equals(other: Any?): Boolean { + return other is RoundedCornersTransform + } + + /** must override */ + override fun hashCode(): Int { + return id.hashCode() + } + + /** must override */ + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(idBytes) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/ScreenUtil.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/ScreenUtil.kt new file mode 100644 index 0000000..d3ed806 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/ScreenUtil.kt @@ -0,0 +1,211 @@ +package org.mozilla.xiu.browser.utils + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.Activity +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Build +import android.util.DisplayMetrics +import android.util.TypedValue +import android.view.View +import android.view.WindowManager +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsControllerCompat +import java.lang.reflect.Method + + +/** + * 作者:By 15968 + * 日期:On 2022/3/4 + * 时间:At 19:34 + */ +object ScreenUtil { + + fun getScreenHeight(activity: Activity): Int { + val manager = activity.windowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + var screenHeight = outMetrics.heightPixels + if (!isOrientation(activity) && outMetrics.widthPixels > screenHeight) { + screenHeight = outMetrics.widthPixels + } + return screenHeight + } + + fun getScreenWidth(activity: Context): Int { + val manager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + var screenWidth = outMetrics.widthPixels + if (!isOrientation(activity) && outMetrics.heightPixels < screenWidth) { + screenWidth = outMetrics.heightPixels + } + return screenWidth + } + + fun getScreenMin(context: Context): Int { + val manager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + return Math.min(outMetrics.widthPixels, outMetrics.heightPixels) + } + + fun getScreenMax(context: Context): Int { + val manager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + return Math.max(outMetrics.widthPixels, outMetrics.heightPixels) + } + + /** + * 获取整个手机屏幕的大小(包括虚拟按钮) + * 必须在onWindowFocus方法之后使用 + * + * @param activity + * @return + */ + fun getScreenSize(activity: Activity): IntArray? { + val size = IntArray(2) + val decorView: View = activity.window.decorView + size[0] = decorView.getWidth() + size[1] = decorView.getHeight() + return size + } + + fun getScreenWidth3(context: Context): Int { + val manager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + return outMetrics.widthPixels + } + + /** + * 获取状态栏的高度 + */ + fun getStatusBarHeight(activity: Activity): Int { + val resources: Resources = activity.resources + val resourceId: Int = resources.getIdentifier("status_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } + + /** + * 获取虚拟按键的高度 + */ + fun getNavigationBarHeight(activity: Activity): Int { + var navigationBarHeight = 0 + val rs: Resources = activity.resources + val id: Int = rs.getIdentifier("navigation_bar_height", "dimen", "android") + if (id > 0 && hasNavigationBar(activity)) { + navigationBarHeight = rs.getDimensionPixelSize(id) + } + return navigationBarHeight + } + + /** + * 是否存在虚拟按键 + * + * @return + */ + private fun hasNavigationBar(activity: Activity): Boolean { + var hasNavigationBar = false + val rs: Resources = activity.resources + val id: Int = rs.getIdentifier("config_showNavigationBar", "bool", "android") + if (id > 0) { + hasNavigationBar = rs.getBoolean(id) + } + try { + @SuppressLint("PrivateApi") val systemPropertiesClass = + Class.forName("android.os.SystemProperties") + val m: Method = systemPropertiesClass.getMethod("get", String::class.java) + val navBarOverride = m.invoke(systemPropertiesClass, "qemu.hw.mainkeys") as String + if ("1" == navBarOverride) { + hasNavigationBar = false + } else if ("0" == navBarOverride) { + hasNavigationBar = true + } + } catch (ignored: Exception) { + } + return hasNavigationBar + } + + fun getDisplayMetrics(activity: Activity): DisplayMetrics? { + return activity + .resources + .displayMetrics + } + + fun toPixels(res: Resources, dp: Float): Int { + return (dp * res.getDisplayMetrics().density) as Int + } + + /** + * Converts sp to px + * + * @param res Resources + * @param sp the value in sp + * @return int + */ + fun toScreenPixels(res: Resources, sp: Float): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, res.getDisplayMetrics()) + .toInt() + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + fun isRtl(res: Resources): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && + res.getConfiguration().getLayoutDirection() === View.LAYOUT_DIRECTION_RTL + } + + fun isTablet(context: Context): Boolean { + return (context.getResources() + .getConfiguration().screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE + && isPad(context)) + } + + fun isPad(context: Context): Boolean { + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val display = wm.defaultDisplay + val dm = DisplayMetrics() + display.getMetrics(dm) + val x = Math.pow((dm.widthPixels / dm.xdpi).toDouble(), 2.0) + val y = Math.pow((dm.heightPixels / dm.ydpi).toDouble(), 2.0) + // 屏幕尺寸 + val screenInches = Math.sqrt(x + y) + // 大于7尺寸则为Pad + return screenInches >= 7.0 + } + + fun isOrientationLand(activity: Context?): Boolean { + return if (activity == null || activity.resources == null) { + false + } else activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + } + + fun isOrientation(activity: Context?): Boolean { + return if (activity == null || activity.resources == null) { + false + } else activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + } + + + private fun getWindowInsetsController(activity: Activity): WindowInsetsControllerCompat? { + return if (Build.VERSION.SDK_INT >= 30) { + //在部分SDK_INT < 30的系统上直接用getWindowInsetsController拿不到 + ViewCompat.getWindowInsetsController(activity.window.decorView) + } else WindowCompat.getInsetsController(activity.window, activity.window.decorView) + } + + fun setDisplayInNotch(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val window = activity.window + // 延伸显示区域到耳朵区 + val lp = window.attributes + lp.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + window.attributes = lp + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/ShareUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/ShareUtil.java new file mode 100644 index 0000000..32cb076 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/ShareUtil.java @@ -0,0 +1,638 @@ +package org.mozilla.xiu.browser.utils; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; + +import androidx.core.content.FileProvider; + +import com.annimon.stream.Stream; + +import java.io.File; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import timber.log.Timber; + +/** + * 作者:By hdy + * 日期:On 2017/10/24 + * 时间:At 19:44 + */ + +public class ShareUtil { + public static void shareText(Context context, String str) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + if (str != null) { + intent.putExtra(Intent.EXTRA_TEXT, str); + } else { + intent.putExtra(Intent.EXTRA_TEXT, ""); + } + intent.setType("text/plain"); + try { + findChooser(context, intent); + } catch (Exception e) { + e.printStackTrace(); + ToastMgr.shortBottomCenter(context, "系统故障:" + e.getMessage()); + } + } + + public static void findVideoPlayerToDeal(Context context, String url) { + if (TextUtils.isEmpty(url)) { + ToastMgr.shortBottomCenter(context, "此链接有问题,不过我们为您复制到了剪贴板"); + ClipboardUtil.copyToClipboard(context, url); + return; + } + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//允许临时的读和写 + Uri content_url = FilesInAppUtil.getUri(context, url); + intent.setDataAndType(content_url, "video/*"); + findChooser(context, intent); + } + + public static void findChooserToDeal(Context context, String url) { + findChooserToDeal(context, url, null); + } + + public static void findChooserToDeal(Context context, String url, String type) { + if (url.startsWith("intent:")) { + try { + Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); + if (intent != null) { + PackageManager packageManager = context.getPackageManager(); + ResolveInfo info = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (info != null) { + context.startActivity(intent); + } else { + ToastMgr.shortBottomCenter(context, "找不到对应的应用"); + } + } + } catch (URISyntaxException e) { + Timber.e(e); + } + return; + } + try { + if (TextUtils.isEmpty(url)) { + ToastMgr.shortBottomCenter(context, "此链接有问题,不过我们为您复制到了剪贴板"); + ClipboardUtil.copyToClipboard(context, url); + return; + } + if (url.startsWith("file:///android_asset")) { + ToastMgr.shortBottomCenter(context, "不支持的链接"); + return; + } + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + //不允许写,不然部分系统安装完软件会自动删除 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);//允许临时的读和写 + Uri content_url = FilesInAppUtil.getUri(context, url); + if (type != null) { + intent.setDataAndType(content_url, type); + } else { + intent.setDataAndType(content_url, getMIMEType(url)); + } + if ("application/vnd.android.package-archive".equals(type) || url.endsWith(".apk")) { + context.startActivity(intent); + } else { + findChooser(context, intent); + } + } catch (Exception e) { + e.printStackTrace(); + ToastMgr.shortBottomCenter(context, "出错:" + e.getMessage()); + } + } + + public static void findChooser(Context context, Intent intent) { + Intent share = UriTool.INSTANCE.getIntentChooser(context, intent, "请选择应用", + componentName -> context.getPackageName().equals(componentName.getPackageName())); + if (share == null) { + intent.setType("*/*"); + context.startActivity(Intent.createChooser(intent, "请选择应用")); + } else { + context.startActivity(share); + } + } + + + private static String getMIMEType(String url) { + String a = url.toLowerCase(); + if (a.contains(".apk")) { + return "application/vnd.android.package-archive"; + } + String type = "*/*"; + if (!a.startsWith("file") && !a.startsWith("/") && !a.startsWith("http")) { + return null; + } + String end = FileUtil.getExtension(url).split("#")[0]; + if (StringUtil.isEmpty(end)) { + return type; + } + Map> map = getMimeTypeMap(); + for (Map.Entry> entry : map.entrySet()) { + for (String s : entry.getValue()) { + if (end.equals(s)) { + return entry.getKey(); + } + } + } + return type; + } + + public static String getMimeTypeByExt(String ext) { + Map> map = getMimeTypeMap(); + for (Map.Entry> entry : map.entrySet()) { + for (String s : entry.getValue()) { + if (ext.equals(s)) { + return entry.getKey(); + } + } + } + return "image/jpeg"; + } + + public static String getExtension(String url, String mimeType) { + if (StringUtil.isEmpty(mimeType)) { + return null; + } + List exts = null; + Map> map = getMimeTypeMap(); + if (map.containsKey(mimeType)) { + exts = map.get(mimeType); + } + if (CollectionUtil.isNotEmpty(exts)) { + if (exts.size() == 1) { + return exts.get(0); + } + if (StringUtil.isNotEmpty(url)) { + //先根据链接匹配,包含优先 + for (String ext : exts) { + if (url.contains("." + ext)) { + return ext; + } + } + } + //链接匹配不上,则取常见的 + if ("text/plain".equals(mimeType)) { + return "txt"; + } + if ("text/html".equals(mimeType)) { + return "html"; + } + if ("audio/mpeg".equals(mimeType)) { + return "mp3"; + } + //最后只能随便取一个 + return exts.get(0); + } + return null; + } + + private static void loadKV(Map> map, String k, String... v) { + if (map.containsKey(k) && map.get(k) != null) { + map.get(k).addAll(Stream.of(v).toList()); + return; + } + map.put(k, Stream.of(v).toList()); + } + + public static Set getExtensions() { + Set exts = new HashSet<>(); + Map> map = getMimeTypeMap(); + for (Map.Entry> entry : map.entrySet()) { + exts.addAll(entry.getValue()); + } + return exts; + } + + public static Map> getMimeTypeMap() { + //正常一个mime-type只对应一个后缀名,但也有写成大写的 + Map> map = new HashMap<>(); + loadKV(map, "application/andrew-inset", "ez"); + loadKV(map, "application/dsptype", "tsp"); + loadKV(map, "application/futuresplash", "spl"); + loadKV(map, "application/hta", "hta"); + loadKV(map, "application/mac-binhex40", "hqx"); + loadKV(map, "application/mac-compactpro", "cpt"); + loadKV(map, "application/mathematica", "nb"); + loadKV(map, "application/msaccess", "mdb"); + loadKV(map, "application/oda", "oda"); + loadKV(map, "application/ogg", "ogg", "ogx"); + loadKV(map, "application/pdf", "pdf"); + loadKV(map, "application/pgp-keys", "key"); + loadKV(map, "application/pgp-signature", "pgp"); + loadKV(map, "application/pics-rules", "prf"); + loadKV(map, "application/rar", "rar"); + loadKV(map, "application/vnd.rar", "rar"); + loadKV(map, "application/rdf+xml", "rdf"); + loadKV(map, "application/rss+xml", "rss"); + loadKV(map, "application/zip", "zip"); + loadKV(map, "application/vnd.android.package-archive", "apk", "apks", "aab", "hap"); + loadKV(map, "application/vnd.cinderella", "cdy"); + loadKV(map, "application/vnd.ms-pki.stl", "stl"); + loadKV(map, "application/vnd.oasis.opendocument.database", "odb"); + loadKV(map, "application/vnd.oasis.opendocument.formula", "odf"); + loadKV(map, "application/vnd.oasis.opendocument.graphics", "odg"); + loadKV(map, "application/vnd.oasis.opendocument.graphics-template", "otg"); + loadKV(map, "application/vnd.oasis.opendocument.image", "odi"); + loadKV(map, "application/vnd.oasis.opendocument.spreadsheet", "ods"); + loadKV(map, "application/vnd.oasis.opendocument.spreadsheet-template", "ots"); + loadKV(map, "application/vnd.oasis.opendocument.text", "odt"); + loadKV(map, "application/vnd.oasis.opendocument.text-master", "odm"); + loadKV(map, "application/vnd.oasis.opendocument.text-template", "ott"); + loadKV(map, "application/vnd.oasis.opendocument.text-web", "oth"); + loadKV(map, "application/msword", "doc"); + loadKV(map, "application/msword", "dot"); + loadKV(map, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"); + loadKV(map, "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "dotx"); + loadKV(map, "application/vnd.ms-excel", "xls"); + loadKV(map, "application/vnd.ms-excel", "xlt"); + loadKV(map, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xlsx"); + loadKV(map, "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "xltx"); + loadKV(map, "application/vnd.ms-powerpoint", "ppt"); + loadKV(map, "application/vnd.ms-powerpoint", "pot"); + loadKV(map, "application/vnd.ms-powerpoint", "pps"); + loadKV(map, "application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"); + loadKV(map, "application/vnd.openxmlformats-officedocument.presentationml.template", + "potx"); + loadKV(map, "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "ppsx"); + loadKV(map, "application/vnd.rim.cod", "cod"); + loadKV(map, "application/vnd.smaf", "mmf"); + loadKV(map, "application/vnd.stardivision.calc", "sdc"); + loadKV(map, "application/vnd.stardivision.draw", "sda"); + loadKV(map, "application/vnd.stardivision.impress", "sdd"); + loadKV(map, "application/vnd.stardivision.impress", "sdp"); + loadKV(map, "application/vnd.stardivision.math", "smf"); + loadKV(map, "application/vnd.stardivision.writer", "sdw"); + loadKV(map, "application/vnd.stardivision.writer", "vor"); + loadKV(map, "application/vnd.stardivision.writer-global", "sgl"); + loadKV(map, "application/vnd.sun.xml.calc", "sxc"); + loadKV(map, "application/vnd.sun.xml.calc.template", "stc"); + loadKV(map, "application/vnd.sun.xml.draw", "sxd"); + loadKV(map, "application/vnd.sun.xml.draw.template", "std"); + loadKV(map, "application/vnd.sun.xml.impress", "sxi"); + loadKV(map, "application/vnd.sun.xml.impress.template", "sti"); + loadKV(map, "application/vnd.sun.xml.math", "sxm"); + loadKV(map, "application/vnd.sun.xml.writer", "sxw"); + loadKV(map, "application/vnd.sun.xml.writer.global", "sxg"); + loadKV(map, "application/vnd.sun.xml.writer.template", "stw"); + loadKV(map, "application/vnd.visio", "vsd"); + loadKV(map, "application/x-abiword", "abw"); + loadKV(map, "application/x-apple-diskimage", "dmg"); + loadKV(map, "application/x-bcpio", "bcpio"); + loadKV(map, "application/x-bittorrent", "torrent"); + loadKV(map, "application/x-cdf", "cdf"); + loadKV(map, "application/x-cdlink", "vcd"); + loadKV(map, "application/x-chess-pgn", "pgn"); + loadKV(map, "application/x-cpio", "cpio"); + loadKV(map, "application/x-debian-package", "deb"); + loadKV(map, "application/x-debian-package", "udeb"); + loadKV(map, "application/x-director", "dcr"); + loadKV(map, "application/x-director", "dir"); + loadKV(map, "application/x-director", "dxr"); + loadKV(map, "application/x-dms", "dms"); + loadKV(map, "application/x-doom", "wad"); + loadKV(map, "application/x-dvi", "dvi"); + loadKV(map, "application/x-flac", "flac"); + loadKV(map, "application/x-font", "pfa"); + loadKV(map, "application/x-font", "pfb"); + loadKV(map, "application/x-font", "gsf"); + loadKV(map, "application/x-font", "pcf"); + loadKV(map, "application/x-font", "pcf.Z"); + loadKV(map, "application/x-freemind", "mm"); + loadKV(map, "application/x-futuresplash", "spl"); + loadKV(map, "application/x-gnumeric", "gnumeric"); + loadKV(map, "application/x-Go-sgf", "sgf"); + loadKV(map, "application/x-graphing-calculator", "gcf"); + loadKV(map, "application/x-gtar", "gtar"); + loadKV(map, "application/x-gtar", "tgz"); + loadKV(map, "application/x-gtar", "taz"); + loadKV(map, "application/x-hdf", "hdf"); + loadKV(map, "application/x-ica", "ica"); + loadKV(map, "application/x-internet-signup", "ins"); + loadKV(map, "application/x-internet-signup", "isp"); + loadKV(map, "application/x-iphone", "iii"); + loadKV(map, "application/x-iso9660-image", "iso"); + loadKV(map, "application/x-jmol", "jmz"); + loadKV(map, "application/x-kchart", "chrt"); + loadKV(map, "application/x-killustrator", "kil"); + loadKV(map, "application/x-koan", "skp"); + loadKV(map, "application/x-koan", "skd"); + loadKV(map, "application/x-koan", "skt"); + loadKV(map, "application/x-koan", "skm"); + loadKV(map, "application/x-kpresenter", "kpr"); + loadKV(map, "application/x-kpresenter", "kpt"); + loadKV(map, "application/x-kspread", "ksp"); + loadKV(map, "application/x-kword", "kwd"); + loadKV(map, "application/x-kword", "kwt"); + loadKV(map, "application/x-latex", "latex"); + loadKV(map, "application/x-lha", "lha"); + loadKV(map, "application/x-lzh", "lzh"); + loadKV(map, "application/x-lzx", "lzx"); + loadKV(map, "application/x-maker", "frm"); + loadKV(map, "application/x-maker", "maker"); + loadKV(map, "application/x-maker", "frame"); + loadKV(map, "application/x-maker", "fb"); + loadKV(map, "application/x-maker", "book"); + loadKV(map, "application/x-maker", "fbdoc"); + loadKV(map, "application/x-mif", "mif"); + loadKV(map, "application/x-ms-wmd", "wmd"); + loadKV(map, "application/x-ms-wmz", "wmz"); + loadKV(map, "application/x-msi", "msi"); + loadKV(map, "application/x-ns-proxy-autoconfig", "pac"); + loadKV(map, "application/x-nwc", "nwc"); + loadKV(map, "application/x-object", "o"); + loadKV(map, "application/x-oz-application", "oza"); + loadKV(map, "application/x-pkcs12", "p12"); + loadKV(map, "application/x-pkcs7-certreqresp", "p7r"); + loadKV(map, "application/x-pkcs7-crl", "crl"); + loadKV(map, "application/x-quicktimeplayer", "qtl"); + loadKV(map, "application/x-shar", "shar"); + loadKV(map, "application/x-shockwave-flash", "swf"); + loadKV(map, "application/x-stuffit", "sit"); + loadKV(map, "application/x-sv4cpio", "sv4cpio"); + loadKV(map, "application/x-sv4crc", "sv4crc"); + loadKV(map, "application/x-tar", "tar"); + loadKV(map, "application/x-texinfo", "texinfo"); + loadKV(map, "application/x-texinfo", "texi"); + loadKV(map, "application/x-troff", "t"); + loadKV(map, "application/x-troff", "roff"); + loadKV(map, "application/x-troff-man", "man"); + loadKV(map, "application/x-ustar", "ustar"); + loadKV(map, "application/x-wais-source", "src"); + loadKV(map, "application/x-wingz", "wz"); + loadKV(map, "application/x-webarchive", "webarchive"); + loadKV(map, "application/x-x509-ca-cert", "crt"); + loadKV(map, "application/x-x509-user-cert", "crt"); + loadKV(map, "application/x-xcf", "xcf"); + loadKV(map, "application/x-xfig", "fig"); + loadKV(map, "application/xhtml+xml", "xhtml"); + loadKV(map, "application/epub+zip", "epub"); + loadKV(map, "audio/3gpp", "3gpp"); + loadKV(map, "audio/amr", "amr"); + loadKV(map, "audio/basic", "snd"); + loadKV(map, "audio/midi", "mid"); + loadKV(map, "audio/midi", "midi"); + loadKV(map, "audio/midi", "kar"); + loadKV(map, "audio/midi", "xmf"); + loadKV(map, "audio/mobile-xmf", "mxmf"); + loadKV(map, "audio/mpeg", "mpga"); + loadKV(map, "audio/mpeg", "mpega"); + loadKV(map, "audio/mpeg", "mp2"); + loadKV(map, "audio/mpeg", "mp3"); + loadKV(map, "audio/mpeg", "m4a"); + loadKV(map, "audio/mpegurl", "m3u"); + loadKV(map, "audio/prs.sid", "sid"); + loadKV(map, "audio/x-aiff", "aif"); + loadKV(map, "audio/x-aiff", "aiff"); + loadKV(map, "audio/x-aiff", "aifc"); + loadKV(map, "audio/x-gsm", "gsm"); + loadKV(map, "audio/x-mpegurl", "m3u"); + loadKV(map, "audio/x-ms-wma", "wma"); + loadKV(map, "audio/x-ms-wax", "wax"); + loadKV(map, "audio/x-pn-realaudio", "ra"); + loadKV(map, "audio/x-pn-realaudio", "rm"); + loadKV(map, "audio/x-pn-realaudio", "ram"); + loadKV(map, "audio/x-realaudio", "ra"); + loadKV(map, "audio/x-scpls", "pls"); + loadKV(map, "audio/x-sd2", "sd2"); + loadKV(map, "audio/x-wav", "wav"); + loadKV(map, "audio/wav", "wav"); + loadKV(map, "image/bmp", "bmp"); + loadKV(map, "image/gif", "gif", "GIF"); + loadKV(map, "image/ico", "cur"); + loadKV(map, "image/ico", "ico"); + loadKV(map, "image/ief", "ief"); + loadKV(map, "image/jpeg", "jpeg", "JPEG"); + loadKV(map, "image/jpeg", "jpg", "JPG"); + loadKV(map, "image/jpeg", "jpe"); + loadKV(map, "image/pcx", "pcx"); + loadKV(map, "image/png", "png", "PNG"); + loadKV(map, "image/svg+xml", "svg"); + loadKV(map, "image/svg+xml", "svgz"); + loadKV(map, "image/tiff", "tiff"); + loadKV(map, "image/tiff", "tif"); + loadKV(map, "image/vnd.djvu", "djvu"); + loadKV(map, "image/vnd.djvu", "djv"); + loadKV(map, "image/vnd.wap.wbmp", "wbmp"); + loadKV(map, "image/x-cmu-raster", "ras"); + loadKV(map, "image/x-coreldraw", "cdr"); + loadKV(map, "image/x-coreldrawpattern", "pat"); + loadKV(map, "image/x-coreldrawtemplate", "cdt"); + loadKV(map, "image/x-corelphotopaint", "cpt"); + loadKV(map, "image/x-icon", "ico"); + loadKV(map, "image/x-jg", "art"); + loadKV(map, "image/x-jng", "jng"); + loadKV(map, "image/x-ms-bmp", "bmp"); + loadKV(map, "image/x-photoshop", "psd"); + loadKV(map, "image/x-portable-anymap", "pnm"); + loadKV(map, "image/x-portable-bitmap", "pbm"); + loadKV(map, "image/x-portable-graymap", "pgm"); + loadKV(map, "image/x-portable-pixmap", "ppm"); + loadKV(map, "image/x-rgb", "rgb"); + loadKV(map, "image/x-xbitmap", "xbm"); + loadKV(map, "image/x-xpixmap", "xpm"); + loadKV(map, "image/x-xwindowdump", "xwd"); + loadKV(map, "model/iges", "igs"); + loadKV(map, "model/iges", "iges"); + loadKV(map, "model/mesh", "msh"); + loadKV(map, "model/mesh", "mesh"); + loadKV(map, "model/mesh", "silo"); + loadKV(map, "text/calendar", "ics"); + loadKV(map, "text/calendar", "icz"); + loadKV(map, "text/csv", "csv"); + loadKV(map, "text/comma-separated-values", "csv"); + loadKV(map, "text/css", "css"); + loadKV(map, "text/html", "htm"); + loadKV(map, "text/html", "html"); + loadKV(map, "text/h323", "323"); + loadKV(map, "text/iuls", "uls"); + loadKV(map, "text/mathml", "mml"); + // add it first so it will be the default for ExtensionFromMimeType + loadKV(map, "text/plain", "txt"); + loadKV(map, "text/plain", "js"); + loadKV(map, "text/javascript", "js"); + loadKV(map, "application/json", "json"); + loadKV(map, "text/plain", "json"); + loadKV(map, "text/plain", "asc"); + loadKV(map, "text/plain", "text"); + loadKV(map, "text/plain", "diff"); + loadKV(map, "text/plain", "po"); // reserve "pot" for vnd.ms-powerpoint + loadKV(map, "text/richtext", "rtx"); + loadKV(map, "text/rtf", "rtf"); + loadKV(map, "text/texmacs", "ts"); + loadKV(map, "video/mp2t", "ts"); + loadKV(map, "text/text", "phps"); + loadKV(map, "text/tab-separated-values", "tsv"); + loadKV(map, "text/xml", "xml"); + loadKV(map, "text/x-bibtex", "bib"); + loadKV(map, "text/x-boo", "boo"); + loadKV(map, "text/x-C++hdr", "h++"); + loadKV(map, "text/x-c++hdr", "hpp"); + loadKV(map, "text/x-c++hdr", "hxx"); + loadKV(map, "text/x-c++hdr", "hh"); + loadKV(map, "text/x-c++src", "c++"); + loadKV(map, "text/x-c++src", "cpp"); + loadKV(map, "text/x-c++src", "cxx"); + loadKV(map, "text/x-chdr", "h"); + loadKV(map, "text/x-component", "htc"); + loadKV(map, "text/x-csh", "csh"); + loadKV(map, "text/x-csrc", "c"); + loadKV(map, "text/x-dsrc", "d"); + loadKV(map, "text/x-haskell", "hs"); + loadKV(map, "text/x-Java", "java"); + loadKV(map, "text/x-literate-haskell", "lhs"); + loadKV(map, "text/x-moc", "moc"); + loadKV(map, "text/x-pascal", "p"); + loadKV(map, "text/x-pascal", "pas"); + loadKV(map, "text/x-pcs-gcd", "gcd"); + loadKV(map, "text/x-setext", "etx"); + loadKV(map, "text/x-tcl", "tcl"); + loadKV(map, "text/x-tex", "tex"); + loadKV(map, "text/x-tex", "ltx"); + loadKV(map, "text/x-tex", "sty"); + loadKV(map, "text/x-tex", "cls"); + loadKV(map, "text/x-vcalendar", "vcs"); + loadKV(map, "text/x-vcard", "vcf"); + loadKV(map, "video/3gpp", "3gpp"); + loadKV(map, "video/3gpp", "3gp"); + loadKV(map, "video/3gpp", "3g2"); + loadKV(map, "video/dl", "dl"); + loadKV(map, "video/dv", "dif"); + loadKV(map, "video/dv", "dv"); + loadKV(map, "video/fli", "fli"); + loadKV(map, "video/m4v", "m4v"); + loadKV(map, "video/mpeg", "mpeg"); + loadKV(map, "video/mpeg", "mpg"); + loadKV(map, "video/mpeg", "mpe"); + loadKV(map, "video/mp4", "mp4", "MP4", "m4s"); + loadKV(map, "video/x-matroska", "mkv", "MKV"); + loadKV(map, "video/x-flv", "flv", "FLV"); + loadKV(map, "video/flv", "flv", "FLV"); + loadKV(map, "video/iso.segment", "m4s"); + loadKV(map, "application/x-mpegURL", "m3u8"); + loadKV(map, "application/vnd.apple.mpegurl", "m3u8"); + loadKV(map, "application/mpegurl", "m3u8"); + loadKV(map, "application/x-mpegurl", "m3u8"); + loadKV(map, "application/x-mpeg", "m3u8"); + loadKV(map, "application/x-msdownload", "exe"); + loadKV(map, "application/wps-office.docx", "wps"); + loadKV(map, "font/ttf", "ttf"); + loadKV(map, "font/collection", "collection"); + loadKV(map, "font/otf", "otf"); + loadKV(map, "font/sfnt", "sfnt"); + loadKV(map, "font/woff2", "woff2"); + loadKV(map, "font/woff", "woff"); + loadKV(map, "application/x-gzip", "gz"); + loadKV(map, "application/x-7z-compressed", "7z"); + loadKV(map, "application/vnd.mozilla.xul+xml", "xul"); + loadKV(map, "image/webp", "webp"); + loadKV(map, "video/webm", "webm"); + loadKV(map, "audio/webm", "weba"); + loadKV(map, "application/x-sh", "sh"); + loadKV(map, "application/x-httpd-php", "php"); + loadKV(map, "audio/opus", "opus"); + loadKV(map, "text/javascript", "mjs"); + loadKV(map, "application/java-archive", "jar"); + loadKV(map, "application/vnd.ms-fontobject", "eot"); + loadKV(map, "application/x-bzip", "bz"); + loadKV(map, "application/octet-stream", "bin", "so", "o", "a", "obb", "bds", "hwt", "mtz", "itz", "nsp"); + loadKV(map, "application/vnd.amazon.ebook", "azw"); + loadKV(map, "application/x-freearc", "arc"); + loadKV(map, "audio/aac", "aac"); + loadKV(map, "application/x-subrip", "srt", "ass"); + loadKV(map, "text/vtt", "vtt"); + loadKV(map, "text/plain", "hiker"); + loadKV(map, "text/markdown", "md"); + loadKV(map, "video/mpeg", "VOB"); + loadKV(map, "video/quicktime", "qt"); + loadKV(map, "video/quicktime", "mov", "MOV"); + loadKV(map, "video/vnd.mpegurl", "mxu"); + loadKV(map, "video/x-la-asf", "lsf"); + loadKV(map, "video/x-la-asf", "lsx"); + loadKV(map, "video/x-mng", "mng"); + loadKV(map, "video/x-ms-asf", "asf"); + loadKV(map, "video/x-ms-asf", "asx"); + loadKV(map, "video/x-ms-wm", "wm"); + loadKV(map, "video/x-ms-wmv", "wmv"); + loadKV(map, "video/x-ms-wmx", "wmx"); + loadKV(map, "video/x-ms-wvx", "wvx"); + loadKV(map, "video/x-msvideo", "avi"); + loadKV(map, "video/x-sgi-movie", "movie"); + loadKV(map, "x-conference/x-cooltalk", "ice"); + loadKV(map, "x-epoc/x-sisx-app", "sisx"); + return map; + } + + + public static void findToDealFileByPath(Context context, String path) { + if (TextUtils.isEmpty(path)) { + ToastMgr.shortBottomCenter(context, "此链接有问题,不过我们为您复制到了剪贴板"); + ClipboardUtil.copyToClipboard(context, path); + return; + } + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//允许临时的读和写 + Uri content_path = FileProvider.getUriForFile(context, "com.hiker.youtoo.provider", new File(path)); + intent.setData(content_path); + findChooser(context, intent); + } + + public static void findChooserToSend(Context context, String url) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.setType("*/*"); + Uri content_url = FilesInAppUtil.getUri(context, url); + intent.putExtra(Intent.EXTRA_STREAM, content_url); + findChooser(context, intent); + } + + public static void chooserMediaPlayer(Context context, String url) { + if (url == null || url.length() < 10) { + ToastMgr.shortBottomCenter(context, "此链接有问题,不过我们为您复制到了剪贴板"); + ClipboardUtil.copyToClipboard(context, url); + return; + } + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + Intent mediaIntent = new Intent(Intent.ACTION_VIEW); + mediaIntent.setDataAndType(Uri.parse(url), mimeType); + findChooser(context, mediaIntent); + } + + public static void toWeChatScan(Context context) { + try { + Uri uri = Uri.parse("weixin://"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);//允许临时的读和写 + context.startActivity(intent); + } catch (Exception e) { + ToastMgr.shortBottomCenter(context, "打开微信失败!"); + } + } + + public static void startUrl(Context context, String url) { + try { + Uri uri = Uri.parse(url); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);//允许临时的读和写 + context.startActivity(intent); + } catch (Exception e) { + ToastMgr.shortBottomCenter(context, "打开失败!"); + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/SizeUtils.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/SizeUtils.kt new file mode 100644 index 0000000..9eaa68e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/SizeUtils.kt @@ -0,0 +1,16 @@ +package org.mozilla.xiu.browser.utils + +import android.content.Context +import android.content.res.Configuration + +fun getSizeName(context: Context): String? { + var screenLayout = context.resources.configuration.screenLayout + screenLayout = screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK + return when (screenLayout) { + Configuration.SCREENLAYOUT_SIZE_SMALL -> "small" + Configuration.SCREENLAYOUT_SIZE_NORMAL -> "normal" + Configuration.SCREENLAYOUT_SIZE_LARGE -> "large" + 4 -> "xlarge" + else -> "undefined" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/SoftKeyBoardListener.java b/app/src/main/java/org/mozilla/xiu/browser/utils/SoftKeyBoardListener.java new file mode 100644 index 0000000..b03e860 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/SoftKeyBoardListener.java @@ -0,0 +1,82 @@ +package org.mozilla.xiu.browser.utils; + + +import android.app.Activity; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewTreeObserver; + +/** + * @describe: 监听软件盘 + * @author: Gao Chunfa + * @time: 2018/2/1-上午9:21 + * @Company: 猎象网络科技 + * @other: + */ +public class SoftKeyBoardListener { + private View rootView;//activity的根视图 + int rootViewVisibleHeight;//纪录根视图的显示高度 + private OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener; + + public SoftKeyBoardListener(Activity activity) { + //获取activity的根视图 + rootView = activity.getWindow().getDecorView(); + + //监听视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变 + rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + //获取当前根视图在屏幕上显示的大小 + Rect r = new Rect(); + rootView.getWindowVisibleDisplayFrame(r); + + int visibleHeight = r.height(); + System.out.println(""+visibleHeight); + if (rootViewVisibleHeight == 0) { + rootViewVisibleHeight = visibleHeight; + return; + } + + //根视图显示高度没有变化,可以看作软键盘显示/隐藏状态没有改变 + if (rootViewVisibleHeight == visibleHeight) { + return; + } + + //根视图显示高度变小超过200,可以看作软键盘显示了 + if (rootViewVisibleHeight - visibleHeight > 200) { + if (onSoftKeyBoardChangeListener != null) { + onSoftKeyBoardChangeListener.keyBoardShow(rootViewVisibleHeight - visibleHeight); + } + rootViewVisibleHeight = visibleHeight; + return; + } + + //根视图显示高度变大超过200,可以看作软键盘隐藏了 + if (visibleHeight - rootViewVisibleHeight > 200) { + if (onSoftKeyBoardChangeListener != null) { + onSoftKeyBoardChangeListener.keyBoardHide(visibleHeight - rootViewVisibleHeight); + } + rootViewVisibleHeight = visibleHeight; + return; + } + + } + }); + } + + private void setOnSoftKeyBoardChangeListener(OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener) { + this.onSoftKeyBoardChangeListener = onSoftKeyBoardChangeListener; + } + + public interface OnSoftKeyBoardChangeListener { + void keyBoardShow(int height); + + void keyBoardHide(int height); + } + + public static void setListener(Activity activity, OnSoftKeyBoardChangeListener onSoftKeyBoardChangeListener) { + SoftKeyBoardListener softKeyBoardListener = new SoftKeyBoardListener(activity); + softKeyBoardListener.setOnSoftKeyBoardChangeListener(onSoftKeyBoardChangeListener); + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/StatusBarCompatUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/StatusBarCompatUtil.java new file mode 100644 index 0000000..e31262e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/StatusBarCompatUtil.java @@ -0,0 +1,49 @@ +package org.mozilla.xiu.browser.utils; + +import android.app.Activity; +import android.content.res.Configuration; +import android.os.Build; + +import com.githang.statusbar.StatusBarCompat; + +/** + * 作者:By 15968 + * 日期:On 2020/6/28 + * 时间:At 20:05 + */ +public class StatusBarCompatUtil { +// public static void setNowColor(int nowColor) { +// StatusBarCompatUtil.nowColor = nowColor; +// } +// +// private static int nowColor; + + public static void setStatusBarColor(Activity activity, int color) { +// if (nowColor == color) { +// return; +// } +// StatusBarCompat.setStatusBarColor(activity, color); +// nowColor = color; + setStatusBarColorForce(activity, color); + } + + public static void setStatusBarColorForce(Activity activity, int color) { + if (isDark(activity)) { + boolean isLightColor = StatusBarCompat.toGrey(color) > 225; + StatusBarCompat.setStatusBarColor(activity, color, !isLightColor); + } else { + StatusBarCompat.setStatusBarColor(activity, color); + } + } + + private static boolean isDark(Activity activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return false; + } + int nightModeFlags = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) { + return true; + } + return false; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/StatusUtils.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/StatusUtils.kt new file mode 100644 index 0000000..8423402 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/StatusUtils.kt @@ -0,0 +1,50 @@ +package org.mozilla.xiu.browser.utils + +import android.app.Activity +import android.content.res.Configuration +import android.os.Build +import android.view.View +import android.view.WindowManager +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import org.mozilla.xiu.browser.R + + +object StatusUtils { + fun init(context: Activity) { + MyStatusBarUtil.setColorNoTranslucent(context, context.resources.getColor(R.color.white)) + } + + fun hideStatusBar(context: Activity) { + if (Build.VERSION.SDK_INT >= 21) { + context.window + .setFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN + ) + } + } + + fun isDarkMode(context: Activity): Boolean { + val mode: Int = + context.getResources().getConfiguration().uiMode and Configuration.UI_MODE_NIGHT_MASK + return mode == Configuration.UI_MODE_NIGHT_YES + } + + /** + * 为啥不直接用setStatusBarVisibilityFullTheme,因为会出现横屏切换竖屏时屏幕左侧出现和状态栏高度一样的白边 + * + * @param visible + */ + fun setStatusBarVisibility(context: Activity, visible: Boolean, rootView: View) { + if (!visible) { + val controllerCompat = WindowCompat.getInsetsController(context.window, rootView) + controllerCompat.hide(WindowInsetsCompat.Type.systemBars()) + return + } else { + val controllerCompat = WindowCompat.getInsetsController(context.window, rootView) + controllerCompat.show(WindowInsetsCompat.Type.systemBars()) + return + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/StrUtil.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/StrUtil.kt new file mode 100644 index 0000000..6713666 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/StrUtil.kt @@ -0,0 +1,515 @@ +package org.mozilla.xiu.browser.utils + +import android.text.TextUtils +import java.io.File +import java.io.UnsupportedEncodingException +import java.util.* +import java.util.regex.Pattern + +/** + * 作者:By 15968 + * 日期:On 2021/10/19 + * 时间:At 14:14 + */ +object StrUtil { + + private val LOWER_CASES = arrayOf("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z") + private val UPPER_CASES = arrayOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z") + private val NUMS_LIST = arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") + private val SYMBOLS_ARRAY = arrayOf("!", "~", "^", "_", "*") + + fun genRandomPwd(pwd_len: Int): String? { + return genRandomPwd(pwd_len, false) + } + + /** + * 生成随机密码 + * + * @param pwd_len 密码长度 + * @param simple 简单模式 + * @return 密码的字符串 + */ + fun genRandomPwd(pwd_len: Int, simple: Boolean): String? { + if (pwd_len < 6 || pwd_len > 20) { + return "" + } + var upper: Int + var num = 0 + var symbol = 0 + var lower: Int = pwd_len / 2 + if (simple) { + upper = pwd_len - lower + } else { + upper = (pwd_len - lower) / 2 + num = (pwd_len - lower) / 2 + symbol = pwd_len - lower - upper - num + } + val pwd = StringBuilder() + val random = Random() + var position = 0 + while (lower + upper + num + symbol > 0) { + if (lower > 0) { + position = random.nextInt(pwd.length + 1) + pwd.insert(position, LOWER_CASES[random.nextInt(LOWER_CASES.size)]) + lower-- + } + if (upper > 0) { + position = random.nextInt(pwd.length + 1) + pwd.insert(position, UPPER_CASES[random.nextInt(UPPER_CASES.size)]) + upper-- + } + if (num > 0) { + position = random.nextInt(pwd.length + 1) + pwd.insert(position, NUMS_LIST[random.nextInt(NUMS_LIST.size)]) + num-- + } + if (symbol > 0) { + position = random.nextInt(pwd.length + 1) + pwd.insert(position, SYMBOLS_ARRAY[random.nextInt(SYMBOLS_ARRAY.size)]) + symbol-- + } + println(pwd.toString()) + } + return pwd.toString() + } + + fun arrayToString(list: Array?, fromIndex: Int, cha: String?): String? { + return arrayToString(list, fromIndex, list?.size ?: 0, cha) + } + + fun arrayToString(list: Array?, fromIndex: Int, endIndex: Int, cha: String?): String? { + val builder = StringBuilder() + if (list == null || list.size <= fromIndex) { + return "" + } else if (list.size <= 1) { + return list[0] + } else { + builder.append(list[fromIndex]) + } + var i = 1 + fromIndex + while (i < list.size && i < endIndex) { + builder.append(cha).append(list[i]) + i++ + } + return builder.toString() + } + + + fun listToString(list: List?, cha: String?): String? { + val builder = StringBuilder() + if (list == null || list.isEmpty()) { + return "" + } else if (list.size <= 1) { + return list[0] + } else { + builder.append(list[0]) + } + for (i in 1 until list.size) { + builder.append(cha).append(list[i]) + } + return builder.toString() + } + + fun listToString(list: List?, fromIndex: Int, cha: String?): String? { + val builder = StringBuilder() + if (list == null || list.size <= fromIndex) { + return "" + } else if (list.size <= 1) { + return list[0] + } else { + builder.append(list[fromIndex]) + } + for (i in fromIndex + 1 until list.size) { + builder.append(cha).append(list[i]) + } + return builder.toString() + } + + fun listToString(list: List?): String? { + return listToString(list, "&&") + } + + fun replaceBlank(str: String?): String? { + return try { + var dest = "" + if (str != null) { + val p = Pattern.compile("\\s*|\t|\r|\n") + val m = p.matcher(str) + dest = m.replaceAll("") + } + dest + } catch (e: Exception) { + str + } + } + + fun replaceLineBlank(str: String?): String? { + return try { + str?.replace("\n".toRegex(), "") + } catch (e: Exception) { + str + } + } + + fun trimBlanks(str: String?): String? { + if (str == null || str.isEmpty()) { + return str + } + str.trim() + var len = str.length + var st = 0 + while (st < len && (str[st] == '\n' || str[st] == '\r' || str[st] == '\t')) { + st++ + } + while (st < len && (str[len - 1] == '\n' || str[len - 1] == '\r' || str[len - 1] == '\t')) { + len-- + } + return if (st > 0 || len < str.length) str.substring(st, len) else str + } + + fun equalsDomUrl(url1: String?, url2: String?): Boolean { + if (url1 == null) { + return url2 == null + } + if (url2 == null) { + return false + } + var pUrl: String = url1 + if (pUrl.endsWith("/")) { + pUrl = pUrl.substring(0, pUrl.length - 1) + } + var sUrl: String = url2 + if (sUrl.endsWith("/")) { + sUrl = sUrl.substring(0, sUrl.length - 1) + } + return pUrl == sUrl + } + + fun getHomeUrl(url: String): String? { + return if (isEmpty(url)) { + url + } else { + val dom = getDom(url) + if (url.startsWith("https")) { + "https://$dom/" + } else { + "http://$dom/" + } + } + } + + + fun isHexStr(str: String): Boolean { + var str = str + var flag = false + if (TextUtils.isEmpty(str)) { + return false + } + if (!str.startsWith("#")) { + str = "#$str" + } + if (str.length != 7 && str.length != 9) { + return false + } + for (i in 1 until str.length) { + val cc = str[i] + if (cc == '0' || cc == '1' || cc == '2' || cc == '3' || cc == '4' || cc == '5' || cc == '6' || cc == '7' || cc == '8' || cc == '9' || cc == 'A' || cc == 'B' || cc == 'C' || cc == 'D' || cc == 'E' || cc == 'F' || cc == 'a' || cc == 'b' || cc == 'c' || cc == 'd' || cc == 'e' || cc == 'f') { + flag = true + } + } + return flag + } + + // 判断一个字符是否是中文 + private fun isChinese(c: Char): Boolean { + return c.toInt() in 0x4E00..0x9FA5 // 根据字节码判断 + } + + // 判断一个字符串是否含有中文 + fun containsChinese(str: String?): Boolean { + if (str == null) return false + for (c in str.toCharArray()) { + if (isChinese(c)) return true + } + return false + } + + fun decodeConflictStr(str: String): String { + return if (isEmpty(str)) { + str + } else str.replace("??", "?").replace("&&", "&").replace(";;", ";") + } + + fun isUrl(str: String): Boolean { + if (TextUtils.isEmpty(str)) { + return false + } + return if (isWebUrl(str)) { + true + } else !containsChinese(str) && str.contains(".") && !str.contains(" ") + } + + fun getDom(u: String?): String? { + if (TextUtils.isEmpty(u)) { + return u + } + var url: String = u!! + try { + url = url.replaceFirst("http://".toRegex(), "").replaceFirst("https://".toRegex(), "") + val urls = url.split("/").toTypedArray() + if (urls.isNotEmpty()) { + return urls[0] + } + } catch (e: Exception) { + return null + } + return url + } + + fun removeDom(u: String?): String? { + if (isEmpty(u)) { + return u + } + var url = u!! + try { + url = url.replaceFirst("http://".toRegex(), "").replaceFirst("https://".toRegex(), "") + val urls: Array = url.split("/").toTypedArray() + if (urls.size > 1) { + return arrayToString(urls, 1, "/") + } + } catch (e: Exception) { + e.printStackTrace() + } + return url + } + + fun getSimpleDom(url: String?): String? { + val dom = getDom(url) + if (isEmpty(url) || isEmpty(dom) || url == dom) { + return url + } + val s = dom!!.split("\\.").toTypedArray() + return if (s.size < 3) { + dom + } else dom.substring(dom.indexOf(".", s.size - 2) + 1) + } + + /** + * 转义正则特殊字符 ($()*+.[]?\^{},|) + * + * @param keyword + * @return keyword + */ + fun escapeExprSpecialWord(keyword: String?): String? { + return if (isEmpty(keyword)) { + val fbsArr = arrayOf("\\", "$", "(", ")", "*", "+", ".", "[", "]", "?", "^", "{", "}", "|") + var k = keyword!! + for (key in fbsArr) { + if (k.contains(key)) { + k = k.replace(key, "\\" + key) + } + } + k + } else { + keyword + } + } + + /** + * 删除正则特殊字符 ($()*+.[]?\^{},|) + * + * @param keyword + * @return keyword + */ + fun removeSpecialWord(keyword: String): String? { + var keyword = keyword + if (!TextUtils.isEmpty(keyword)) { + val fbsArr = arrayOf("\\", "$", "(", ")", "*", "+", ".", "[", "]", "?", "^", "{", "}", "|") + for (key in fbsArr) { + if (keyword.contains(key)) { + keyword = keyword.replace(key, "") + } + } + } + return keyword + } + + private val FilePattern = Pattern.compile("[\\\\/:*?\"<>|]") + + fun filenameFilter(str: String?): String? { + return if (str == null) null else FilePattern.matcher(str).replaceAll("_").replace(File.separator, "_") + } + + fun getBaseUrl(url: String): String { + if (isEmpty(url)) { + return url + } + val baseUrls = url.replace("http://", "").replace("https://", "") + val baseUrl2 = baseUrls.split("/").toTypedArray()[0] + return if (url.startsWith("https")) { + "https://$baseUrl2" + } else { + "http://$baseUrl2" + } + } + + fun isEmpty(str: CharSequence?): Boolean { + return str == null || str.isEmpty() + } + + fun isNotEmpty(str: CharSequence?): Boolean { + return !isEmpty(str) + } + + fun isUTF8(str: String): Boolean { + return try { + str.toByteArray(charset("utf-8")) + true + } catch (e: UnsupportedEncodingException) { + false + } + } + + + fun convertBlankToTagP(content: String): String? { + return try { + if (isEmpty(content)) { + content + } else if (!content.contains("\n")) { + content + } else { + content.replace("\n", "
") + } + } catch (e: Exception) { + content + } + } + + + fun simplyGroup(title: String): String { + return if (isEmpty(title)) { + title + } else title.replace("①", "") + .replace("②", "") + .replace("③", "") + .replace("④", "") + .replace("⑤", "") + .replace("⑥", "") + .replace("⑦", "") + .replace("⑧", "") + .replace("⑨", "") + .replace("⑩", "") + } + + /** + * 判读是否是emoji + * + * @param codePoint + * @return + */ + fun getIsEmoji(codePoint: Char): Boolean { + return !(codePoint.toInt() == 0x0 || codePoint.toInt() == 0x9 || codePoint.toInt() == 0xA + || codePoint.toInt() == 0xD + || codePoint.toInt() in 0x20..0xD7FF + || codePoint.toInt() in 0xE000..0xFFFD + || codePoint.toInt() in 0x10000..0x10FFFF) + } + + + fun getIsSp(codePoint: Char): Boolean { + return Character.getType(codePoint) > Character.LETTER_NUMBER + } + + /** + * 判断搜索框内容是否包含特殊字符 + * + * @param str + * @return + */ + fun hasSpWord(str: String?): Boolean { + val limitEx = "[`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@①#¥%……&*()——+|{}【】‘;:”“’。,、?]" + val pattern = Pattern.compile(limitEx) + val m = pattern.matcher(str) + return m.find() + } + + /** + * 判断是否只含英文数字 + * + * @param str + * @return + */ + fun isLetterDigit(str: String): Boolean { + val regex = "^[a-z0-9A-Z\\-_]+$" + return str.matches(Regex(regex)) + } + + fun isWebUrl(str: String): Boolean { + if (isEmpty(str) || str.contains(" ")) { + return false + } + val url = str.toLowerCase() + return (url.startsWith("http") || url.startsWith("file://") || url.startsWith("ftp") + || url.startsWith("rtmp://") || url.startsWith("rtsp://")) + } + + + fun autoFixUrl(b: String?, u: String?): String? { + if (isEmpty(b) || isEmpty(u)) { + return u + } + var bUrl = b!! + val url = u!! + bUrl = bUrl.split(";").toTypedArray()[0] + val baseUrl = getBaseUrl(bUrl) + val lowUrl = url.toLowerCase() + return if (lowUrl.startsWith("http") || lowUrl.startsWith("hiker") || lowUrl.startsWith("pics") || lowUrl.startsWith("code")) { + url + } else if (url.startsWith("//")) { + "http:$url" + } else if (url.startsWith("magnet") || url.startsWith("thunder") || url.startsWith("ftp") || url.startsWith("ed2k")) { + url + } else if (url.startsWith("/")) { + if (baseUrl.endsWith("/")) { + baseUrl.substring(0, baseUrl.length - 1) + url + } else { + baseUrl + url + } + } else if (url.startsWith("./")) { + val protocolUrl = bUrl.split("://").toTypedArray() + if (protocolUrl.isEmpty()) { + return url + } + val c = protocolUrl[1].split("/").toTypedArray() + if (c.size <= 1) { + return if (baseUrl.endsWith("/")) { + baseUrl.substring(0, baseUrl.length - 1) + url.replace("./", "") + } else { + baseUrl + url.replace("./", "") + } + } + val sub = protocolUrl[1].replace(c[c.size - 1], "") + protocolUrl[0] + "://" + sub + url.replace("./", "") + } else if (url.startsWith("?")) { + bUrl + url + } else { + url + } + } + + fun splitUrlByQuestionMark(url: String): Array? { + return if (isEmpty(url)) { + arrayOf(url) + } else { + val urls: Array = url.split("\\?").toTypedArray() + if (urls.size <= 1) { + urls + } else { + val res = arrayOfNulls(2) + res[0] = urls[0] + res[1] = arrayToString(urls, 1, "?") + res + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/StringUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/StringUtil.java new file mode 100644 index 0000000..2e293c3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/StringUtil.java @@ -0,0 +1,859 @@ +package org.mozilla.xiu.browser.utils; + +import android.text.TextUtils; + + +import androidx.annotation.Nullable; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 作者:By hdy + * 日期:On 2018/11/12 + * 时间:At 12:03 + */ +public class StringUtil { + + public final static String[] LOWER_CASES = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}; + public final static String[] UPPER_CASES = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}; + public final static String[] NUMS_LIST = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + public final static String[] SYMBOLS_ARRAY = {"!", "~", "^", "_", "*"}; + + public final static String SCHEME_DOWNLOAD = "download://"; + public final static String SCHEME_EDIT_FILE = "editFile://"; + public final static String SCHEME_OPEN_FILE = "openFile://"; + public final static String SCHEME_COPY = "copy://"; + public final static String SCHEME_SELECT = "select://"; + public final static String SCHEME_CONFIRM = "confirm://"; + public final static String SCHEME_INPUT = "input://"; + public final static String SCHEME_WEB = "web://"; + public final static String SCHEME_TOAST = "toast://"; + public final static String SCHEME_SHARE = "share://"; + public final static String SCHEME_FILE_SELECT = "fileSelect://"; + + private static final String[] SCHEME_CANT_HANDLE = new String[]{"ftp:", "ed2k:", "magnet:", "thunder:", "xg:"}; + + private final static String[] innerScheme = {"http", "hiker", "file", "/", "content", "{", "[", "x5:", SCHEME_FILE_SELECT, SCHEME_TOAST, SCHEME_WEB, SCHEME_INPUT, + SCHEME_CONFIRM, SCHEME_SELECT, SCHEME_COPY, "x5WebView:", SCHEME_EDIT_FILE, "rtmp:", "rtsp:", "rule:", "code:", "海阔视界", "x5Play:", "func:", + SCHEME_DOWNLOAD, SCHEME_SHARE, SCHEME_OPEN_FILE, "webview://"}; + + public static boolean isCannotHandleScheme(String url) { + for (String s : SCHEME_CANT_HANDLE) { + if (url.startsWith(s)) { + return true; + } + } + return false; + } + + public static boolean isScheme(String lowUrl) { + if (lowUrl.startsWith("video://")) { + return false; + } + for (String s : innerScheme) { + if (lowUrl.startsWith(s)) { + return false; + } + } + String[] urls = lowUrl.split("://"); + if (urls.length < 2) { + return false; + } + String scheme = urls[0]; + if (scheme.length() > 20) { + return false; + } + return isOnlyEn(scheme); + } + + public static String genRandomPwd(int pwd_len) { + return genRandomPwd(pwd_len, false); + } + + /** + * 生成随机密码 + * + * @param pwd_len 密码长度 + * @param simple 简单模式 + * @return 密码的字符串 + */ + public static String genRandomPwd(int pwd_len, boolean simple) { + if (pwd_len < 2 || pwd_len > 20) { + return ""; + } + int lower, upper, num = 0, symbol = 0; + lower = pwd_len / 2; + + if (simple) { + upper = pwd_len - lower; + } else { + upper = (pwd_len - lower) / 2; + num = (pwd_len - lower) / 2; + symbol = pwd_len - lower - upper - num; + } + + StringBuilder pwd = new StringBuilder(); + Random random = new Random(); + int position = 0; + while ((lower + upper + num + symbol) > 0) { + if (lower > 0) { + position = random.nextInt(pwd.length() + 1); + + pwd.insert(position, LOWER_CASES[random.nextInt(LOWER_CASES.length)]); + lower--; + } + if (upper > 0) { + position = random.nextInt(pwd.length() + 1); + + pwd.insert(position, UPPER_CASES[random.nextInt(UPPER_CASES.length)]); + upper--; + } + if (num > 0) { + position = random.nextInt(pwd.length() + 1); + + pwd.insert(position, NUMS_LIST[random.nextInt(NUMS_LIST.length)]); + num--; + } + if (symbol > 0) { + position = random.nextInt(pwd.length() + 1); + + pwd.insert(position, SYMBOLS_ARRAY[random.nextInt(SYMBOLS_ARRAY.length)]); + symbol--; + } + + System.out.println(pwd.toString()); + } + return pwd.toString(); + } + + public static String arrayToString(String[] list, int fromIndex, String cha) { + return arrayToString(list, fromIndex, list == null ? 0 : list.length, cha); + } + + public static String arrayToString(String[] list, int fromIndex, int endIndex, String cha) { + return StrUtil.INSTANCE.arrayToString(list, fromIndex, endIndex, cha); + } + + public static String listToString(List list, String cha) { + return StrUtil.INSTANCE.listToString(list, cha); + } + + public static String listToString(List list, int fromIndex, String cha) { + return StrUtil.INSTANCE.listToString(list, fromIndex, cha); + } + + public static String listToString(List list) { + return listToString(list, "&&"); + } + + public static String replaceBlank(String str) { + try { + String dest = ""; + if (str != null) { + Pattern p = Pattern.compile("\\s*|\t|\r|\n"); + Matcher m = p.matcher(str); + dest = m.replaceAll(""); + } + return dest; + } catch (Exception e) { + return str; + } + } + + public static String replaceLineBlank(String str) { + try { + return str.replaceAll("\n", ""); + } catch (Exception e) { + return str; + } + } + + public static String trimBlanks(String str) { + if (str == null || str.length() == 0) { + return str; + } + int len = str.length(); + int st = 0; + + while ((st < len) && (str.charAt(st) == '\n' || str.charAt(st) == '\r' || str.charAt(st) == '\f' || str.charAt(st) == '\t')) { + st++; + } + while ((st < len) && (str.charAt(len - 1) == '\n' || str.charAt(len - 1) == '\r' || str.charAt(len - 1) == '\f' || str.charAt(len - 1) == '\t')) { + len--; + } + return ((st > 0) || (len < str.length())) ? str.substring(st, len) : str; + } + + public static String trimAll(String str) { + if (str == null || str.length() == 0) { + return str; + } + int len = str.length(); + int st = 0; + //中文空格 + while ((st < len) && (str.charAt(st) == '\n' || str.charAt(st) == '\r' || str.charAt(st) == '\f' || str.charAt(st) == '\t' || str.charAt(st) == ' ' || str.charAt(st) == ' ')) { + st++; + } + while ((st < len) && (str.charAt(len - 1) == '\n' || str.charAt(len - 1) == '\r' || str.charAt(len - 1) == '\f' || str.charAt(len - 1) == '\t' || str.charAt(len - 1) == ' ' || str.charAt(len - 1) == ' ')) { + len--; + } + return ((st > 0) || (len < str.length())) ? str.substring(st, len) : str; + } + + public static String clearLine(String str) { + String[] s = str.split("\n"); + int st = 0; + for (int i = 0; i < s.length; i++) { + if (i == 0) { + while ((st < s[0].length()) && str.charAt(st) == ' ') { + st++; + } + } + if (st > 0 && st < s[i].length()) { + s[i] = s[i].substring(st); + } + } + return arrayToString(s, 0, "\n"); + } + + public static boolean equalsDomUrl(String url1, String url2) { + if (url1 == null) { + return url2 == null; + } + if (url2 == null) { + return false; + } + String pUrl = url1; + if (pUrl.endsWith("/")) { + pUrl = pUrl.substring(0, pUrl.length() - 1); + } + String sUrl = url2; + if (sUrl.endsWith("/")) { + sUrl = sUrl.substring(0, sUrl.length() - 1); + } + return pUrl.equals(sUrl); + } + + public static String getHomeUrl(String url) { + return getHome(url); + } + + + public static boolean isHexStr(String str) { + boolean flag = false; + if (TextUtils.isEmpty(str)) { + return false; + } + if (!str.startsWith("#")) { + str = "#" + str; + } + if (str.length() != 7 && str.length() != 9) { + return false; + } + for (int i = 1; i < str.length(); i++) { + char cc = str.charAt(i); + if (cc == '0' || cc == '1' || cc == '2' || cc == '3' || cc == '4' || cc == '5' || cc == '6' || cc == '7' || cc == '8' || cc == '9' || cc == 'A' || cc == 'B' || cc == 'C' || + cc == 'D' || cc == 'E' || cc == 'F' || cc == 'a' || cc == 'b' || cc == 'c' || cc == 'd' || cc == 'e' || cc == 'f') { + flag = true; + } + } + return flag; + } + + // 判断一个字符是否是中文 + private static boolean isChinese(char c) { + return c >= 0x4E00 && c <= 0x9FA5;// 根据字节码判断 + } + + // 判断一个字符串是否含有中文 + public static boolean containsChinese(String str) { + if (str == null) + return false; + for (char c : str.toCharArray()) { + if (isChinese(c)) + return true; + } + return false; + } + + // 判断一个字符串是否全是中文 + public static boolean isAllChinese(String str) { + if (str == null) { + return true; + } + for (char c : str.toCharArray()) { + if (!isChinese(c)) { + return false; + } + } + return true; + } + + public static String decodeConflictStr(String str) { + if (isEmpty(str)) { + return str; + } + return str.replace("??", "?").replace("&&", "&").replace(";;", ";"); + } + + public static boolean isUrl(String str) { + if (TextUtils.isEmpty(str)) { + return false; + } + if (isWebUrl(str)) { + return true; + } + return !containsChinese(str) && str.contains(".") && !str.contains(" "); + } + + public static String getRealUrl(String pageUrl, String url) { + url = getRealUrl(url); + if (StringUtil.isEmpty(url) || !url.startsWith("http")) { + return pageUrl; + } + return url; + } + + public static String getRealUrl(String url) { + if (TextUtils.isEmpty(url)) { + return url; + } + try { + String maxUrl = getMaxItem(url, "\\$\\$\\$"); + if (isNotEmpty(maxUrl)) { + return maxUrl; + } + maxUrl = getMaxItem(url, "\\$\\$"); + if (isNotEmpty(maxUrl)) { + return maxUrl; + } + maxUrl = getMaxItem(url, "##"); + if (isNotEmpty(maxUrl)) { + return maxUrl; + } + return url; + } catch (Exception e) { + e.printStackTrace(); + return url; + } + } + + private static String getMaxItem(String url, String sep) { + String[] a = url.split(sep); + String maxUrl = null; + int max = 0; + for (String s : a) { + if (s.startsWith("http")) { + if (s.length() > max) { + max = s.length(); + maxUrl = s; + } + } + } + return maxUrl; + } + + public static String getDom(String url) { + if (TextUtils.isEmpty(url)) { + return url; + } + return getDom0(getRealUrl(url)); + } + + private static String getDom0(String url) { + if (TextUtils.isEmpty(url)) { + return url; + } + if (url.startsWith("thunder://") || url.startsWith("magnet:") || url.startsWith("ftp://") || url.startsWith("ed2k:") || url.startsWith("file://")) { + return url; + } + try { + String[] s = url.split("://"); + if (s.length > 1) { + return s[1].split("/")[0]; + } + String[] s2 = url.split("//"); + if (s2.length > 1) { + return s2[1].split("/")[0]; + } + url = url.replaceFirst("http://", "").replaceFirst("https://", ""); + String[] urls = url.split("/"); + if (urls.length > 0) { + return urls[0]; + } + } catch (Exception e) { + return null; + } + return url; + } + + public static String getSecondDom(String url) { + if (isEmpty(url)) { + return url; + } + String dom = getDom(url); + String[] doms = dom.split("\\."); + if (doms.length >= 3) { + //二级域名 + return StringUtil.arrayToString(doms, doms.length - 2, "."); + } + return dom; + } + + public static String getHome(String url) { + if (TextUtils.isEmpty(url)) { + return url; + } + url = getRealUrl(url); + String dom = getDom0(url); + return url.split("://")[0] + "://" + dom; + } + + public static String removeDom(String url) { + if (TextUtils.isEmpty(url)) { + return url; + } + try { + url = url.replaceFirst("http://", "").replaceFirst("https://", ""); + String[] urls = url.split("/"); + if (urls.length > 1) { + return arrayToString(urls, 1, "/"); + } + } catch (Exception e) { + e.printStackTrace(); + } + return url; + } + + public static String getSimpleDom(String url) { + String dom = getDom(url); + if (StringUtil.isEmpty(url) || StringUtil.isEmpty(dom) || url.equals(dom)) { + return url; + } + String[] s = dom.split("\\."); + if (s.length < 3) { + return dom; + } + return dom.substring(dom.indexOf(".", s.length - 2) + 1); + } + + /** + * 多关键字查询表红,避免后面的关键字成为特殊的HTML语言代码 + * + * @param str 检索结果 + * @param inputs 关键字集合 + * @param resStr 表红后的结果 + */ + public static StringBuilder spannableString(String str, List inputs, StringBuilder resStr) { + int index = str.length();//用来做为标识,判断关键字的下标 + String next = "";//保存str中最先找到的关键字 + for (int i = inputs.size() - 1; i >= 0; i--) { + String theNext = inputs.get(i); + int theIndex = str.indexOf(theNext); + if (theIndex == -1) {//过滤掉无效关键字 + inputs.remove(i); + } else if (theIndex < index) { + index = theIndex;//替换下标 + next = theNext; + } + } + //如果条件成立,表示串中已经没有可以被替换的关键字,否则递归处理 + if (index == str.length()) { + resStr.append(str); + } else { + resStr.append(str.substring(0, index)); + resStr.append("").append(str.substring(index, index + next.length())).append(""); + String str1 = str.substring(index + next.length(), str.length()); + spannableString(str1, inputs, resStr);//剩余的字符串继续替换 + } + return resStr; + } + + /** + * 转义正则特殊字符 ($()*+.[]?\^{},|) + * + * @param keyword + * @return keyword + */ + public static String escapeExprSpecialWord(String keyword) { + if (!TextUtils.isEmpty(keyword)) { + String[] fbsArr = {"\\", "$", "(", ")", "*", "+", ".", "[", "]", "?", "^", "{", "}", "|"}; + for (String key : fbsArr) { + if (keyword.contains(key)) { + keyword = keyword.replace(key, "\\" + key); + } + } + } + return keyword; + } + + /** + * 删除正则特殊字符 ($()*+.[]?\^{},|) + * + * @param keyword + * @return keyword + */ + + public static String removeSpecialWord(String keyword) { + if (!TextUtils.isEmpty(keyword)) { + String[] fbsArr = {"\\", "$", "(", ")", "*", "+", ".", "[", "]", "?", "^", "{", "}", "|"}; + for (String key : fbsArr) { + if (keyword.contains(key)) { + keyword = keyword.replace(key, ""); + } + } + } + return keyword; + } + + private static Pattern FilePattern = Pattern.compile("[\\\\/:*?\"<>|]"); + + public static String filenameFilter(String str) { + if (StringUtil.isNotEmpty(str) && str.startsWith("http")) { + return md5(str); + } + String s = str == null ? null : FilePattern.matcher(str).replaceAll("_") + .replace(File.separator, "_") + .replace("...", "_") + .replace("\n", "_") + .replace("\r", "_"); + return s == null ? "" : (s.length() > 85 ? s.substring(0, 42) + "-" + s.substring(s.length() - 42) : s); + } + + public static String getBaseUrl(String url) { + if (StringUtil.isEmpty(url)) { + return url; + } + String baseUrls = url.replace("http://", "").replace("https://", ""); + String baseUrl2 = baseUrls.split("/")[0]; + String baseUrl; + if (url.startsWith("https")) { + baseUrl = "https://" + baseUrl2; + } else { + baseUrl = "http://" + baseUrl2; + } + return baseUrl; + } + + public static boolean isEmpty(@Nullable CharSequence str) { + return str == null || str.length() == 0; + } + + public static boolean isNotEmpty(@Nullable CharSequence str) { + return !isEmpty(str); + } + + public static boolean isUTF8(String str) { + try { + str.getBytes("utf-8"); + return true; + } catch (UnsupportedEncodingException e) { + return false; + } + + } + + + public static String convertBlankToTagP(String content) { + try { + if (StringUtil.isEmpty(content)) { + return content; + } else if (!content.contains("\n")) { + return content; + } else { + return content.replace("\n", "
"); + } + } catch (Exception e) { + return content; + } + } + + + public static String simplyGroup(String title) { + if (isEmpty(title)) { + return title; + } + return StrUtil.INSTANCE.simplyGroup(title); + } + + /** + * 判读是否是emoji + * + * @param codePoint + * @return + */ + public static boolean getIsEmoji(char codePoint) { + return StrUtil.INSTANCE.getIsEmoji(codePoint); + } + + + public static boolean getIsSp(char codePoint) { + return Character.getType(codePoint) > Character.LETTER_NUMBER; + } + + /** + * 判断搜索框内容是否包含特殊字符 + * + * @param str + * @return + */ + public static boolean hasSpWord(String str) { + String limitEx = "[`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@①#¥%……&*()——+|{}【】‘;:”“’。,、?]"; + Pattern pattern = Pattern.compile(limitEx); + Matcher m = pattern.matcher(str); + return m.find(); + } + + /** + * 判断是否只含英文数字 + * + * @param str + * @return + */ + public static boolean isLetterDigit(String str) { + String regex = "^[a-z0-9A-Z\\-._]+$"; + return str.matches(regex); + } + + + public static boolean isWebUrl(String str) { + if (isEmpty(str)) { + return false; + } + if (!isEmpty(str) && (str.startsWith("http://") || str.startsWith("https://")) && !str.contains(" ")) { + return true; + } + String url = str.toLowerCase(); + return url.startsWith("http") || url.startsWith("file://") || url.startsWith("ftp") + || url.startsWith("rtmp://") || url.startsWith("rtsp://") || url.startsWith("magnet:?") || url.startsWith("thunder:"); + } + + /** + * 判断是否只含英文字母 + * + * @param str + * @return + */ + public static boolean isOnlyEn(String str) { + String regex = "^[a-zA-Z]+$"; + return str.matches(regex); + } + + public static String autoFixUrl(String bUrl, String url) { + if (isEmpty(bUrl) || isEmpty(url)) { + return url; + } + bUrl = bUrl.split(";")[0]; + String baseUrl = getBaseUrl(bUrl); + String lowUrl = url.toLowerCase(); + if (lowUrl.startsWith("http") || lowUrl.startsWith("hiker") || lowUrl.startsWith("pics") || lowUrl.startsWith("code")) { + return url; + } else if (url.startsWith("//")) { + return "http:" + url; + } else if (url.startsWith("magnet") || url.startsWith("thunder") || url.startsWith("ftp") || url.startsWith("ed2k")) { + return url; + } else if (url.startsWith("/")) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + url; + } else { + return baseUrl + url; + } + } else if (url.startsWith("./")) { + String[] protocolUrl = bUrl.split("://"); + if (protocolUrl.length < 1) { + return url; + } + String[] c = protocolUrl[1].split("/"); + if (c.length <= 1) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + url.replace("./", ""); + } else { + return baseUrl + url.replace("./", ""); + } + } + String sub = protocolUrl[1].replace(c[c.length - 1], ""); + return protocolUrl[0] + "://" + sub + url.replace("./", ""); + } else if (url.startsWith("?")) { + return bUrl + url; + } else { + return url; + } + } + + public static String[] splitUrlByQuestionMark(String url) { + if (isEmpty(url)) { + return new String[]{url}; + } else { + String[] urls = url.split("\\?"); + if (urls.length <= 1) { + return urls; + } else { + String[] res = new String[2]; + res[0] = urls[0]; + res[1] = arrayToString(urls, 1, "?"); + return res; + } + } + } + + public static byte[] hexToBytes(String hex) { + if (hex.length() < 1) { + return null; + } else { + byte[] result = new byte[hex.length() / 2]; + int j = 0; + for (int i = 0; i < hex.length(); i += 2) { + result[j++] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16); + } + return result; + } + } + + public static String md5(String source) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + messageDigest.update(source.getBytes()); + return convertByte2String(messageDigest.digest()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return source; + } + + private static String convertByte2String(byte[] byteResult) { + char[] hexDigits = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + // 4位代表一个16进制,所以长度需要变为原来2倍 + char[] result = new char[byteResult.length * 2]; + int index = 0; + for (byte b : byteResult) { + // 先转换高4位 + result[index++] = hexDigits[(b >>> 4) & 0xf]; + result[index++] = hexDigits[b & 0xf]; + } + return new String(result); + } + + + public static boolean searchContains(String text, String key, boolean ignoreCase) { + if (isEmpty(text)) return false; + if (isEmpty(key)) return true; + if (ignoreCase) { + text = text.toLowerCase(); + key = key.toLowerCase(); + } + String[] tmp2 = key.split(" "); + if (tmp2.length >= 2) { + String u = text; + for (String s : tmp2) { + int i = u.indexOf(s); + if (i < 0) { + return false; + } + u = u.substring(i + s.length()); + } + return true; + } else { + return text.contains(key); + } + } + + + public static String replaceContains(String text, String key, + String wrapStart, String wrapEnd, + String wrapStart2, String wrapEnd2) { + if (isEmpty(text)) return text; + if (isEmpty(key)) return text; + String[] tmp2 = key.split(" "); + if (tmp2.length >= 1) { + String u = text; + List list = new ArrayList<>(); + for (String s : tmp2) { + int i = u.indexOf(s); + if (i < 0) { + break; + } + String pre = u.substring(0, i); + if (isNotEmpty(wrapStart2)) { + list.add(wrapStart2); + } + list.add(pre); + if (isNotEmpty(wrapEnd2)) { + list.add(wrapEnd2); + } + list.add(wrapStart); + list.add(s); + list.add(wrapEnd); + u = u.substring(i + s.length()); + } + if (isNotEmpty(u)) { + if (isNotEmpty(wrapStart2)) { + list.add(wrapStart2); + } + list.add(u); + if (isNotEmpty(wrapEnd2)) { + list.add(wrapEnd2); + } + } + return CollectionUtil.listToString(list, ""); + } else { + return text; + } + } + + public static String keepNums(String text) { + if (isEmpty(text)) return text; + return text.replaceAll("[^\\d]", ""); + } + + /** + * 莱文斯坦距离,又称 Levenshtein 距离,是编辑距离的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。 + * + * @param a + * @param b + * @return + */ + public static float levenshtein(String a, String b) { + if (a == null && b == null) { + return 1f; + } + if (a == null || b == null) { + return 0F; + } + int editDistance = editDis(a, b); + return 1 - ((float) editDistance / Math.max(a.length(), b.length())); + } + + private static int editDis(String a, String b) { + + int aLen = a.length(); + int bLen = b.length(); + + if (aLen == 0) return aLen; + if (bLen == 0) return bLen; + + int[][] v = new int[aLen + 1][bLen + 1]; + for (int i = 0; i <= aLen; ++i) { + for (int j = 0; j <= bLen; ++j) { + if (i == 0) { + v[i][j] = j; + } else if (j == 0) { + v[i][j] = i; + } else if (a.charAt(i - 1) == b.charAt(j - 1)) { + v[i][j] = v[i - 1][j - 1]; + } else { + v[i][j] = 1 + Math.min(v[i - 1][j - 1], Math.min(v[i][j - 1], v[i - 1][j])); + } + } + } + return v[aLen][bLen]; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/ThreadTool.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/ThreadTool.kt new file mode 100644 index 0000000..a2f245c --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/ThreadTool.kt @@ -0,0 +1,193 @@ +package org.mozilla.xiu.browser.utils + +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.function.Consumer +import kotlin.coroutines.EmptyCoroutineContext + +/** + * 作者:By 15968 + * 日期:On 2021/10/24 + * 时间:At 20:29 + */ +object ThreadTool { + + val scopeMap: MutableMap by lazy { + HashMap() + } + + fun executeNewTask(command: Runnable?): Job { + return GlobalScope.launch(Dispatchers.IO) { + command?.run() + } + } + + fun async(task: Runnable) { + executeNewTask(task) + } + + fun async(task: Runnable, uiTask: Consumer?) { + GlobalScope.launch(Dispatchers.IO) { + try { + task.run() + if (uiTask != null) { + withContext(Dispatchers.Main) { + uiTask.accept(null) + } + } + } catch (e: Exception) { + if (uiTask != null) { + withContext(Dispatchers.Main) { + uiTask.accept(e) + } + } + } + } + } + + fun async(block: suspend CoroutineScope.() -> Unit) { + GlobalScope.launch(Dispatchers.IO, block = block) + } + + fun newScope(): CoroutineScope { + return CoroutineScope(EmptyCoroutineContext) + } + + fun cancelScope(scope: CoroutineScope) { + try { + scope.cancel() + } catch (e: Throwable) { + e.printStackTrace() + } + } + + fun launch(scope: CoroutineScope, task: Runnable) { + try { + scope.launch(Dispatchers.IO) { + task.run() + } + } catch (e: Throwable) { + e.printStackTrace() + } + } + + fun postDelayed(delayMillSec: Long, task: Runnable) { + try { + GlobalScope.launch(Dispatchers.IO) { + delay(delayMillSec) + task.run() + } + } catch (e: Throwable) { + e.printStackTrace() + } + } + + fun postUIDelayed(delayMillSec: Long, task: Runnable) { + try { + GlobalScope.launch(Dispatchers.Main) { + delay(delayMillSec) + task.run() + } + } catch (e: Throwable) { + e.printStackTrace() + } + } + + fun runOnUI(task: Runnable) { + if (Looper.myLooper() == Looper.getMainLooper()) { + task.run() + return + } + GlobalScope.launch(Dispatchers.Main) { + task.run() + } + } + + fun runOnUI(scope: CoroutineScope, task: Runnable) { + try { + scope.launch(Dispatchers.Main) { + task.run() + } + } catch (e: Throwable) { + e.printStackTrace() + } + } + + fun loadScheduleTask(holder: Any, scheduleGap: Long, runnable: Runnable): CoroutineScope { + if (scopeMap.containsKey(holder) && scopeMap[holder] != null && scopeMap[holder]!!.isActive) { + scopeMap[holder]?.launch(Dispatchers.IO) { + try { + while (isActive) { + runnable.run() + delay(scheduleGap) + } + } catch (e: Exception) { + } + } + return scopeMap[holder]!! + } + val scope = newScope() + scope.launch(Dispatchers.IO) { + try { + while (isActive) { + runnable.run() + delay(scheduleGap) + } + } catch (e: Exception) { + } + } + scopeMap[holder] = scope + return scope + } + + fun newScope(holder: Any): CoroutineScope { + val scope = CoroutineScope(EmptyCoroutineContext) + scopeMap[holder] = scope + return scope + } + + fun cancelTasks(holder: Any) { + try { + if (scopeMap.containsKey(holder)) { + scopeMap[holder]?.cancel() + scopeMap.remove(holder) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun getStrOnUIThread(runnable: Consumer): String? { + val urlHolder = UrlHolder() + if (Thread.currentThread() === Looper.getMainLooper().thread) { + runnable.accept(urlHolder) + return urlHolder.url + } + val countDownLatch = CountDownLatch(1) + runOnUI { + runnable.accept(urlHolder) + countDownLatch.countDown() + } + try { + countDownLatch.await(5, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + e.printStackTrace() + } + return urlHolder.url + } + + class UrlHolder { + var url: String? = null + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/TimeUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/TimeUtil.java new file mode 100644 index 0000000..f42c405 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/TimeUtil.java @@ -0,0 +1,62 @@ +package org.mozilla.xiu.browser.utils; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 作者:By 15968 + * 日期:On 2019/12/7 + * 时间:At 12:13 + */ +public class TimeUtil { + public static String formatTime(long timestamp) { + DateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); + return formatter.format(new Date(timestamp)); + } + + public static String formatTime(long timestamp, String pattern) { + DateFormat formatter = new SimpleDateFormat(pattern); + return formatter.format(new Date(timestamp)); + } + + + public static String getSecondTimestamp() { + return getSecondTimestamp(new Date(System.currentTimeMillis())); + } + + /** + * 获取精确到秒的时间戳 + * + * @return + */ + public static String getSecondTimestamp(Date date) { + if (null == date) { + return ""; + } + String timestamp = String.valueOf(date.getTime()); + int length = timestamp.length(); + if (length > 3) { + return timestamp.substring(0, length - 3); + } else { + return timestamp; + } + } + + /** + * 秒数转换成格式化时间 + * + * @param sec 秒数 + * @return xx 时 xx 分 xx 秒 + */ + public static String secToTime(int sec) { + if (sec <= 0) { + return "0 秒"; + } else { + int hour = sec / 3600; + int minute = sec % 3600 / 60; + int second = sec % 60; // 不足 60 的就是秒,够 60 就是分 + return (hour > 0 ? (hour + " 小时 ") : "") + (minute > 0 ? (minute + " 分 ") : "") + (second + " 秒"); + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/ToastMgr.java b/app/src/main/java/org/mozilla/xiu/browser/utils/ToastMgr.java new file mode 100644 index 0000000..dc9a717 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/ToastMgr.java @@ -0,0 +1,62 @@ +package org.mozilla.xiu.browser.utils; + +/** + * 作者:By hdy + * 日期:On 2017/11/6 + * 时间:At 16:51 + */ + +import android.content.Context; +import android.os.Looper; +import android.view.Gravity; +import android.widget.Toast; + +public class ToastMgr { + + public static void shortBottomCenter(Context context, String message) { + if (context == null) { + return; + } + runOnUiThread(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show()); + } + + public static void shortCenter(Context context, String message) { + if (context == null) { + return; + } + runOnUiThread(() -> { + Toast toast = Toast.makeText(context, message, Toast.LENGTH_SHORT); + toast.setGravity(Gravity.CENTER, 0, 0); + toast.show(); + }); + } + + public static void longCenter(Context context, String message) { + if (context == null) { + return; + } + runOnUiThread(() -> { + Toast toast = Toast.makeText(context, message, Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, 0); + toast.show(); + }); + } + + public static void longBottomCenter(Context context, String message) { + if (context == null) { + return; + } + runOnUiThread(() -> { + Toast toast = Toast.makeText(context, message, Toast.LENGTH_LONG); + toast.show(); + }); + } + + private static void runOnUiThread(Runnable runnable) { + if (Looper.myLooper() == Looper.getMainLooper()) { + runnable.run(); + } else { + ThreadTool.INSTANCE.runOnUI(runnable); + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/UUIDUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/UUIDUtil.java new file mode 100644 index 0000000..af4b6c8 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/UUIDUtil.java @@ -0,0 +1,13 @@ +package org.mozilla.xiu.browser.utils; + +import java.util.UUID; + +/** + * Created by xm on 15/4/23. + */ +public class UUIDUtil { + + public static String genUUID(){ + return UUID.randomUUID().toString().replaceAll("-", ""); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/UriTool.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/UriTool.kt new file mode 100644 index 0000000..9883a7a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/UriTool.kt @@ -0,0 +1,114 @@ +package org.mozilla.xiu.browser.utils + +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.pm.LabeledIntent +import android.net.Uri +import android.os.Build +import android.os.Parcelable +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import java.io.File + +/** + * 作者:By 15968 + * 日期:On 2022/1/19 + * 时间:At 14:24 + */ +object UriTool { + @SuppressLint("Range") + fun uriToFileName(uri: Uri, context: Context): String { + return when (uri.scheme) { + ContentResolver.SCHEME_FILE -> File(uri.path!!).name + ContentResolver.SCHEME_CONTENT -> { + try { + val cursor = context.contentResolver.query(uri, null, null, null, null, null) + cursor?.let { + it.moveToFirst() + val displayName = + it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + cursor.close() + displayName + } ?: "${System.currentTimeMillis()}.${ + MimeTypeMap.getSingleton() + .getExtensionFromMimeType(context.contentResolver.getType(uri)) + }}" + } catch (e: Exception) { + "${System.currentTimeMillis()}.${ + MimeTypeMap.getSingleton() + .getExtensionFromMimeType(context.contentResolver.getType(uri)) + }}" + } + + } + else -> "${System.currentTimeMillis()}.${ + MimeTypeMap.getSingleton() + .getExtensionFromMimeType(context.contentResolver.getType(uri)) + }}" + } + } + + fun getIntentChooser( + context: Context, + intent: Intent, + chooserTitle: CharSequence? = null, + filter: ComponentNameFilter + ): Intent? { + val resolveInfos = context.packageManager.queryIntentActivities(intent, 0) +// Log.d("AppLog", "found apps to handle the intent:") + val excludedComponentNames = HashSet() + resolveInfos.forEach { + val activityInfo = it.activityInfo + val componentName = ComponentName(activityInfo.packageName, activityInfo.name) +// Log.d("AppLog", "componentName:$componentName") + if (filter.shouldBeFilteredOut(componentName)) + excludedComponentNames.add(componentName) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return Intent.createChooser(intent, chooserTitle) + .putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, excludedComponentNames.toTypedArray()) + } + if (resolveInfos.isNotEmpty()) { + val targetIntents: MutableList = ArrayList() + for (resolveInfo in resolveInfos) { + val activityInfo = resolveInfo.activityInfo + if (excludedComponentNames.contains( + ComponentName( + activityInfo.packageName, + activityInfo.name + ) + ) + ) + continue + val targetIntent = Intent(intent) + targetIntent.setPackage(activityInfo.packageName) + targetIntent.component = ComponentName(activityInfo.packageName, activityInfo.name) + // wrap with LabeledIntent to show correct name and icon + val labeledIntent = LabeledIntent( + targetIntent, + activityInfo.packageName, + resolveInfo.labelRes, + resolveInfo.icon + ) + // add filtered intent to a list + targetIntents.add(labeledIntent) + } + // deal with M list seperate problem + val chooserIntent: Intent = Intent.createChooser(intent, chooserTitle) + // add initial intents + chooserIntent.putExtra( + Intent.EXTRA_INITIAL_INTENTS, + targetIntents.toTypedArray() + ) + return chooserIntent + } + return null + } +} + +interface ComponentNameFilter { + fun shouldBeFilteredOut(componentName: ComponentName): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/UriUtils.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/UriUtils.kt new file mode 100644 index 0000000..49d7bb1 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/UriUtils.kt @@ -0,0 +1,50 @@ +package org.mozilla.xiu.browser.utils + +import android.content.ContentResolver +import android.net.Uri +import android.webkit.URLUtil +import androidx.annotation.NonNull +import org.mozilla.geckoview.WebResponse +import org.mozilla.xiu.browser.download.getDispositionFileName + + +object UriUtils { + fun isUriContentScheme(@NonNull uri: Uri): Boolean { + return uri.scheme == ContentResolver.SCHEME_CONTENT + } + + fun isUriFileScheme(@NonNull uri: Uri): Boolean { + return uri.scheme == ContentResolver.SCHEME_FILE + } + + + fun getFileName(url: String): String { + var filename = URLUtil.guessFileName(url, null, null) + filename = filename.substring( + 0, + filename.lastIndexOf(".") + ) + "_${System.currentTimeMillis()}" + filename.substring(filename.lastIndexOf(".")) + return filename + } + + fun getFileName(response: WebResponse): String { + var fileName: String? = null + val contentDispositionHeader: String? + contentDispositionHeader = if (response.headers.containsKey("content-disposition")) { + response.headers["content-disposition"] + } else { + response.headers["Content-Disposition"] + } + if (contentDispositionHeader != null && !contentDispositionHeader.isEmpty()) { + fileName = getDispositionFileName(contentDispositionHeader) + } + if (StringUtil.isEmpty(fileName)) { + fileName = FileUtil.getResourceName(response.uri) + } + if (StringUtil.isEmpty(fileName)) { + fileName = "unknown-" + UUIDUtil.genUUID() + } + return fileName ?: "" + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/UriUtilsPro.java b/app/src/main/java/org/mozilla/xiu/browser/utils/UriUtilsPro.java new file mode 100644 index 0000000..3b17e8f --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/UriUtilsPro.java @@ -0,0 +1,256 @@ +package org.mozilla.xiu.browser.utils; + +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.text.TextUtils; + +import org.mozilla.xiu.browser.App; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; + +/** + * 作者:By hdy + * 日期:On 2019/4/29 + * 时间:At 22:20 + */ +public class UriUtilsPro { + /** + * Get a file path from a Uri. This will get the the path for Storage Access + * Framework Documents, as well as the _data field for the MediaStore and + * other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + */ + public static String getPath(final Context context, final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String column = "_data"; + final String[] projection = { + column + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + public static void getFilePathFromURI(Context context, Uri contentUri, LoadListener loadListener) { + getFilePathFromURI(context, contentUri, getRootDir(context) + File.separator + getFileName(contentUri), loadListener); + } + + public static void getFilePathFromURI(final Context context, final Uri contentUri, final String copyToFilePath, final LoadListener loadListener) { + ThreadTool.INSTANCE.executeNewTask(new Runnable() { + @Override + public void run() { + try { + String path = getFilePathFromURI(context, contentUri, copyToFilePath); + loadListener.success(path); + } catch (Exception e) { + loadListener.failed(e.getMessage()); + } + } + }); + } + + /** + * 从content获取路径 + * + * @param context context + * @param contentUri contentUri + * @return + */ + private static String getFilePathFromURI(Context context, Uri contentUri, String copyToFilePath) throws Exception { + String fileName = getFileName(contentUri); + if (!TextUtils.isEmpty(fileName)) { + File copyFile = new File(copyToFilePath); + if (copyFile.getParentFile() != null && !copyFile.getParentFile().exists()) { + copyFile.getParentFile().mkdirs(); + } + copyFile(context, contentUri, copyFile); + return copyFile.getAbsolutePath(); + } + return null; + } + + public static String getRootDir(Context context) { + if (!Objects.requireNonNull(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)).exists()) { + Objects.requireNonNull(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)).mkdir(); + } + return Objects.requireNonNull(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)).getAbsolutePath(); + } + + public static String getCacheDir(Context context) { + String path = getRootDir(context) + File.separator + "cache"; + File file = new File(path); + if (!file.exists()) { + file.mkdirs(); + } + return path; + } + + public static String getFileName(Uri uri) { + if (uri == null) return null; + return UriTool.INSTANCE.uriToFileName(uri, App.application); + } + + private static void copyFile(Context context, Uri srcUri, File dstFile) throws Exception { + try (InputStream inputStream = context.getContentResolver().openInputStream(srcUri)) { + if (inputStream == null) return; + try (OutputStream outputStream = new FileOutputStream(dstFile)) { + copyStream(inputStream, outputStream); + } + } + } + + private static int copyStream(InputStream input, OutputStream output) throws Exception { + final int BUFFER_SIZE = 1024 * 2; + byte[] buffer = new byte[BUFFER_SIZE]; + BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE); + BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE); + int count = 0, n = 0; + try { + while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) { + out.write(buffer, 0, n); + count += n; + } + out.flush(); + } finally { + try { + out.close(); + } catch (IOException e) { + } + try { + in.close(); + } catch (IOException e) { + } + } + return count; + } + + public interface LoadListener { + void success(String s); + + void failed(String msg); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/Utils.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/Utils.kt new file mode 100644 index 0000000..d72539f --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/Utils.kt @@ -0,0 +1,107 @@ +package org.mozilla.xiu.browser.utils + +import android.annotation.SuppressLint +import android.content.ClipboardManager +import android.content.Context +import android.content.res.Configuration +import android.graphics.Rect +import android.media.AudioAttributes +import android.os.Build +import android.os.Vibrator +import android.text.TextPaint +import android.widget.Toast +import androidx.compose.ui.graphics.Color + + +/** + * https://github.com/shehuan/GroupIndexLib + */ + + +object Utils { + //根据手机的分辨率从dp的单位转成px(像素) + fun dip2px(context: Context, dpValue: Int): Int { + //获取当前手机的像素密度(1个dp对应几个px) + val scale = context.resources.displayMetrics.density + //四舍五入取整 + return (dpValue * scale + 0.5f).toInt() + } + fun sp2px(context: Context, spValue: Int): Int { + val fontScale = context.resources.displayMetrics.scaledDensity + return (spValue * fontScale + 0.5f).toInt() + } + + /** + * 测量字符高度 + * + * @param text + * @return + */ + fun getTextHeight(textPaint: TextPaint, text: String): Int { + val bounds = Rect() + textPaint.getTextBounds(text, 0, text.length, bounds) + return bounds.height() + } + fun isPortraitMode(context: Context) : Boolean{ + val mConfiguration: Configuration = context.resources.configuration //获取设置的配置信息 + return mConfiguration.orientation == Configuration.ORIENTATION_PORTRAIT + } + /** + * 测量字符宽度 + * + * @param textPaint + * @param text + * @return + */ + fun getTextWidth(textPaint: TextPaint, text: String?): Int { + return textPaint.measureText(text).toInt() + } + + fun listIsEmpty(list: List?): Boolean { + return list == null || list.size == 0 + } + + @SuppressLint("ServiceCast") + fun copyToClipboard(context: Context, content: String?) { + // 从 API11 开始 android 推荐使用 android.content.ClipboardManager + // 为了兼容低版本我们这里使用旧版的 android.text.ClipboardManager,虽然提示 deprecated,但不影响使用。 + val cm: ClipboardManager = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + // 将文本内容放到系统剪贴板里。 + cm.setText(content) + Toast.makeText(context, "已复制到剪切板", Toast.LENGTH_SHORT).show() + } + + fun Int.requireColor(context: Context): androidx.compose.ui.graphics.Color = Color(context.getColor(this)) + + /** + * 手机震动 + * + * @param context + * @param isRepeat 是否重复震动 + */ + fun playVibrate(context: Context, isRepeat: Boolean) { + + try { + val mVibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + val patern = longArrayOf(100,200,100) + var audioAttributes: AudioAttributes? = null + /** + * 适配android7.0以上版本的震动 + * 说明:如果发现5.0或6.0版本在app退到后台之后也无法震动,那么只需要改下方的Build.VERSION_CODES.N版本号即可 + */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ALARM) //key + .build() + mVibrator.vibrate(patern, if (isRepeat) 1 else -1, audioAttributes) + } else { + mVibrator.vibrate(patern, if (isRepeat) 1 else -1) + } + } catch (ex: Exception) { + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/ZipUtils.java b/app/src/main/java/org/mozilla/xiu/browser/utils/ZipUtils.java new file mode 100644 index 0000000..d1b91c9 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/ZipUtils.java @@ -0,0 +1,448 @@ +package org.mozilla.xiu.browser.utils; + +import android.os.Build; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2016/08/27
+ *     desc  : utils about zip
+ * 
+ */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class ZipUtils { + + private static final int BUFFER_LEN = 8192; + + private ZipUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * Zip the files. + * + * @param srcFiles The source of files. + * @param zipFilePath The path of ZIP file. + * @return {@code true}: success
{@code false}: fail + * @throws IOException if an I/O error has occurred + */ + public static boolean zipFiles(final Collection srcFiles, + final String zipFilePath) + throws IOException { + return zipFiles(srcFiles, zipFilePath, null); + } + + /** + * Zip the files. + * + * @param srcFilePaths The paths of source files. + * @param zipFilePath The path of ZIP file. + * @param comment The comment. + * @return {@code true}: success
{@code false}: fail + * @throws IOException if an I/O error has occurred + */ + public static boolean zipFiles(final Collection srcFilePaths, + final String zipFilePath, + final String comment) + throws IOException { + if (srcFilePaths == null || zipFilePath == null) return false; + ZipOutputStream zos = null; + try { + zos = new ZipOutputStream(new FileOutputStream(zipFilePath)); + for (String srcFile : srcFilePaths) { + if (!zipFile(getFileByPath(srcFile), "", zos, comment)) return false; + } + return true; + } finally { + if (zos != null) { + zos.finish(); + zos.close(); + } + } + } + + /** + * Zip the files. + * + * @param srcFiles The source of files. + * @param zipFile The ZIP file. + * @return {@code true}: success
{@code false}: fail + * @throws IOException if an I/O error has occurred + */ + public static boolean zipFiles(final Collection srcFiles, final File zipFile) + throws IOException { + return zipFiles(srcFiles, zipFile, null); + } + + /** + * Zip the files. + * + * @param srcFiles The source of files. + * @param zipFile The ZIP file. + * @param comment The comment. + * @return {@code true}: success
{@code false}: fail + * @throws IOException if an I/O error has occurred + */ + public static boolean zipFiles(final Collection srcFiles, + final File zipFile, + final String comment) + throws IOException { + if (srcFiles == null || zipFile == null) return false; + ZipOutputStream zos = null; + try { + zos = new ZipOutputStream(new FileOutputStream(zipFile)); + for (File srcFile : srcFiles) { + if (!zipFile(srcFile, "", zos, comment)) return false; + } + return true; + } finally { + if (zos != null) { + zos.finish(); + zos.close(); + } + } + } + + /** + * Zip the file. + * + * @param srcFilePath The path of source file. + * @param zipFilePath The path of ZIP file. + * @return {@code true}: success
{@code false}: fail + * @throws IOException if an I/O error has occurred + */ + public static boolean zipFile(final String srcFilePath, + final String zipFilePath) + throws IOException { + return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), null); + } + + /** + * Zip the file. + * + * @param srcFilePath The path of source file. + * @param zipFilePath The path of ZIP file. + * @param comment The comment. + * @return {@code true}: success
{@code false}: fail + * @throws IOException if an I/O error has occurred + */ + public static boolean zipFile(final String srcFilePath, + final String zipFilePath, + final String comment) + throws IOException { + return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), comment); + } + + /** + * Zip the file. + * + * @param srcFile The source of file. + * @param zipFile The ZIP file. + * @return {@code true}: success
{@code false}: fail + * @throws IOException if an I/O error has occurred + */ + public static boolean zipFile(final File srcFile, + final File zipFile) + throws IOException { + return zipFile(srcFile, zipFile, null); + } + + /** + * Zip the file. + * + * @param srcFile The source of file. + * @param zipFile The ZIP file. + * @param comment The comment. + * @return {@code true}: success
{@code false}: fail + * @throws IOException if an I/O error has occurred + */ + public static boolean zipFile(final File srcFile, + final File zipFile, + final String comment) + throws IOException { + if (srcFile == null || zipFile == null) return false; + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + return zipFile(srcFile, "", zos, comment); + } + } + + private static boolean zipFile(final File srcFile, + String rootPath, + final ZipOutputStream zos, + final String comment) throws IOException { + if (!srcFile.exists()) return true; + rootPath = rootPath + (isSpace(rootPath) ? "" : File.separator) + srcFile.getName(); + if (srcFile.isDirectory()) { + File[] fileList = srcFile.listFiles(); + if (fileList == null || fileList.length <= 0) { + String[] ss = rootPath.split(File.separator); + String entryPath = rootPath.substring(ss[0].length() + 1); + ZipEntry entry = new ZipEntry(entryPath + "/"); + entry.setComment(comment); + zos.putNextEntry(entry); + zos.closeEntry(); + } else { + for (File file : fileList) { + if (!zipFile(file, rootPath, zos, comment)) return false; + } + } + } else { + try (InputStream is = new BufferedInputStream(new FileInputStream(srcFile))) { + //去掉第一层目录,免得a.zip解压成a,再压缩变成了a/a/xxx多一层目录 + String[] ss = rootPath.split(File.separator); + String entryPath = rootPath.substring(ss[0].length() + 1); + ZipEntry entry = new ZipEntry(entryPath); + entry.setComment(comment); + zos.putNextEntry(entry); + byte buffer[] = new byte[BUFFER_LEN]; + int len; + while ((len = is.read(buffer, 0, BUFFER_LEN)) != -1) { + zos.write(buffer, 0, len); + } + zos.closeEntry(); + } + } + return true; + } + + /** + * Unzip the file. + * + * @param zipFilePath The path of ZIP file. + * @param destDirPath The path of destination directory. + * @return the unzipped files + * @throws IOException if unzip unsuccessfully + */ + public static List unzipFile(final String zipFilePath, final String destDirPath) throws IOException { + return unzipFileByKeyword(zipFilePath, destDirPath, null); + } + + /** + * Unzip the file. + * + * @param zipFile The ZIP file. + * @param destDir The destination directory. + * @return the unzipped files + * @throws IOException if unzip unsuccessfully + */ + public static List unzipFile(final File zipFile, + final File destDir) + throws IOException { + return unzipFileByKeyword(zipFile, destDir, null); + } + + /** + * Unzip the file by keyword. + * + * @param zipFilePath The path of ZIP file. + * @param destDirPath The path of destination directory. + * @param keyword The keyboard. + * @return the unzipped files + * @throws IOException if unzip unsuccessfully + */ + public static List unzipFileByKeyword(final String zipFilePath, + final String destDirPath, + final String keyword) throws IOException { + return unzipFileByKeyword(getFileByPath(zipFilePath), getFileByPath(destDirPath), keyword); + } + + /** + * Unzip the file by keyword. + * + * @param zipFile The ZIP file. + * @param destDir The destination directory. + * @param keyword The keyboard. + * @return the unzipped files + * @throws IOException if unzip unsuccessfully + */ + public static List unzipFileByKeyword(final File zipFile, + final File destDir, + final String keyword) throws IOException { + return unzipFileByKeyword0(zipFile, destDir, keyword, false); + } + + public static List unzipFileByKeyword0(final File zipFile, + final File destDir, + final String keyword, boolean gbk) throws IOException { + if (zipFile == null || destDir == null) return null; + List files = new ArrayList<>(); + ZipFile zip = gbk && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? new ZipFile(zipFile, ZipFile.OPEN_READ, Charset.forName("GBK")) : new ZipFile(zipFile); + Enumeration entries = zip.entries(); + try { + if (isSpace(keyword)) { + while (entries.hasMoreElements()) { + try { + ZipEntry entry = ((ZipEntry) entries.nextElement()); + String entryName = entry.getName(); + if (entryName.contains("../")) { + Log.e("ZipUtils", "entryName: " + entryName + " is dangerous!"); + continue; + } + if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files; + } catch (IllegalArgumentException e) { + if (!gbk && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return unzipFileByKeyword0(zipFile, destDir, keyword, true); + } + } + } + } else { + while (entries.hasMoreElements()) { + ZipEntry entry = ((ZipEntry) entries.nextElement()); + String entryName = entry.getName(); + if (entryName.contains("../")) { + Log.e("ZipUtils", "entryName: " + entryName + " is dangerous!"); + continue; + } + if (entryName.contains(keyword)) { + if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files; + } + } + } + } finally { + zip.close(); + } + return files; + } + + private static boolean unzipChildFile(final File destDir, + final List files, + final ZipFile zip, + final ZipEntry entry, + final String name) throws IOException { + File file = new File(destDir, name); + files.add(file); + if (entry.isDirectory()) { + return createOrExistsDir(file); + } else { + if (!createOrExistsFile(file)) return false; + try (InputStream in = new BufferedInputStream(zip.getInputStream(entry)); OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) { + byte buffer[] = new byte[BUFFER_LEN]; + int len; + while ((len = in.read(buffer)) != -1) { + out.write(buffer, 0, len); + } + } + } + return true; + } + + /** + * Return the files' path in ZIP file. + * + * @param zipFilePath The path of ZIP file. + * @return the files' path in ZIP file + * @throws IOException if an I/O error has occurred + */ + public static List getFilesPath(final String zipFilePath) + throws IOException { + return getFilesPath(getFileByPath(zipFilePath)); + } + + /** + * Return the files' path in ZIP file. + * + * @param zipFile The ZIP file. + * @return the files' path in ZIP file + * @throws IOException if an I/O error has occurred + */ + public static List getFilesPath(final File zipFile) + throws IOException { + if (zipFile == null) return null; + List paths = new ArrayList<>(); + ZipFile zip = new ZipFile(zipFile); + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + String entryName = ((ZipEntry) entries.nextElement()).getName(); + if (entryName.contains("../")) { + Log.e("ZipUtils", "entryName: " + entryName + " is dangerous!"); + paths.add(entryName); + } else { + paths.add(entryName); + } + } + zip.close(); + return paths; + } + + /** + * Return the files' comment in ZIP file. + * + * @param zipFilePath The path of ZIP file. + * @return the files' comment in ZIP file + * @throws IOException if an I/O error has occurred + */ + public static List getComments(final String zipFilePath) + throws IOException { + return getComments(getFileByPath(zipFilePath)); + } + + /** + * Return the files' comment in ZIP file. + * + * @param zipFile The ZIP file. + * @return the files' comment in ZIP file + * @throws IOException if an I/O error has occurred + */ + public static List getComments(final File zipFile) + throws IOException { + if (zipFile == null) return null; + List comments = new ArrayList<>(); + ZipFile zip = new ZipFile(zipFile); + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = ((ZipEntry) entries.nextElement()); + comments.add(entry.getComment()); + } + zip.close(); + return comments; + } + + private static boolean createOrExistsDir(final File file) { + return file != null && (file.exists() ? file.isDirectory() : file.mkdirs()); + } + + private static boolean createOrExistsFile(final File file) { + if (file == null) return false; + if (file.exists()) return file.isFile(); + if (!createOrExistsDir(file.getParentFile())) return false; + try { + return file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + private static File getFileByPath(final String filePath) { + return isSpace(filePath) ? null : new File(filePath); + } + + private static boolean isSpace(final String s) { + if (s == null) return true; + for (int i = 0, len = s.length(); i < len; ++i) { + if (!Character.isWhitespace(s.charAt(i))) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Base64InputStream.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Base64InputStream.java new file mode 100644 index 0000000..3e6227c --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Base64InputStream.java @@ -0,0 +1,283 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Performs Base-64 decoding on an underlying stream. + */ +public class Base64InputStream extends InputStream { + private static final int ENCODED_BUFFER_SIZE = 1536; + + private static final int[] BASE64_DECODE = new int[256]; + + static { + for (int i = 0; i < 256; i++) + BASE64_DECODE[i] = -1; + for (int i = 0; i < Base64OutputStream.BASE64_TABLE.length; i++) + BASE64_DECODE[Base64OutputStream.BASE64_TABLE[i] & 0xff] = i; + } + + private static final byte BASE64_PAD = '='; + + private static final int EOF = -1; + + private final byte[] singleByte = new byte[1]; + + private final InputStream in; + private final byte[] encoded; + private final ByteArrayBuffer decodedBuf; + + private int position = 0; // current index into encoded buffer + private int size = 0; // current size of encoded buffer + + private boolean closed = false; + private boolean eof; // end of file or pad character reached + + private final DecodeMonitor monitor; + + public Base64InputStream(InputStream in, DecodeMonitor monitor) { + this(ENCODED_BUFFER_SIZE, in, monitor); + } + + protected Base64InputStream(int bufsize, InputStream in, DecodeMonitor monitor) { + if (in == null) + throw new IllegalArgumentException(); + this.encoded = new byte[bufsize]; + this.decodedBuf = new ByteArrayBuffer(512); + this.in = in; + this.monitor = monitor; + } + + public Base64InputStream(InputStream in) { + this(in, false); + } + + public Base64InputStream(InputStream in, boolean strict) { + this(ENCODED_BUFFER_SIZE, in, strict ? DecodeMonitor.STRICT : DecodeMonitor.SILENT); + } + + @Override + public int read() throws IOException { + if (closed) + throw new IOException("Stream has been closed"); + + while (true) { + int bytes = read0(singleByte, 0, 1); + if (bytes == EOF) + return EOF; + + if (bytes == 1) + return singleByte[0] & 0xff; + } + } + + @Override + public int read(byte[] buffer) throws IOException { + if (closed) + throw new IOException("Stream has been closed"); + + if (buffer == null) + throw new NullPointerException(); + + if (buffer.length == 0) + return 0; + + return read0(buffer, 0, buffer.length); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (closed) + throw new IOException("Stream has been closed"); + + if (buffer == null) + throw new NullPointerException(); + + if (offset < 0 || length < 0 || offset + length > buffer.length) + throw new IndexOutOfBoundsException(); + + if (length == 0) + return 0; + + return read0(buffer, offset, length); + } + + @Override + public void close() throws IOException { + if (closed) + return; + + closed = true; + } + + private int read0(final byte[] buffer, final int off, final int len) throws IOException { + int to = off + len; + int index = off; + + // check if a previous invocation left decoded content + if (decodedBuf.length() > 0) { + int chunk = Math.min(decodedBuf.length(), len); + System.arraycopy(decodedBuf.buffer(), 0, buffer, index, chunk); + decodedBuf.remove(0, chunk); + index += chunk; + } + + // eof or pad reached? + + if (eof) + return index == off ? EOF : index - off; + + // decode into given buffer + + int data = 0; // holds decoded data; up to four sextets + int sextets = 0; // number of sextets + + while (index < to) { + // make sure buffer not empty + + while (position == size) { + int n = in.read(encoded, 0, encoded.length); + if (n == EOF) { + eof = true; + + if (sextets != 0) { + // error in encoded data + handleUnexpectedEof(sextets); + } + + return index == off ? EOF : index - off; + } else if (n > 0) { + position = 0; + size = n; + } else { + assert n == 0; + } + } + + // decode buffer + + while (position < size && index < to) { + int value = encoded[position++] & 0xff; + + if (value == BASE64_PAD) { + index = decodePad(data, sextets, buffer, index, to); + return index - off; + } + + int decoded = BASE64_DECODE[value]; + if (decoded < 0) { // -1: not a base64 char + if (value != 0x0D && value != 0x0A && value != 0x20) { + if (monitor.warn("Unexpected base64 byte: "+(byte) value, "ignoring.")) + throw new IOException("Unexpected base64 byte"); + } + continue; + } + + data = (data << 6) | decoded; + sextets++; + + if (sextets == 4) { + sextets = 0; + + byte b1 = (byte) (data >>> 16); + byte b2 = (byte) (data >>> 8); + byte b3 = (byte) data; + + if (index < to - 2) { + buffer[index++] = b1; + buffer[index++] = b2; + buffer[index++] = b3; + } else { + if (index < to - 1) { + buffer[index++] = b1; + buffer[index++] = b2; + decodedBuf.append(b3); + } else if (index < to) { + buffer[index++] = b1; + decodedBuf.append(b2); + decodedBuf.append(b3); + } else { + decodedBuf.append(b1); + decodedBuf.append(b2); + decodedBuf.append(b3); + } + + assert index == to; + return to - off; + } + } + } + } + + assert sextets == 0; + assert index == to; + return to - off; + } + + private int decodePad(int data, int sextets, final byte[] buffer, + int index, final int end) throws IOException { + eof = true; + + if (sextets == 2) { + // one byte encoded as "XY==" + + byte b = (byte) (data >>> 4); + if (index < end) { + buffer[index++] = b; + } else { + decodedBuf.append(b); + } + } else if (sextets == 3) { + // two bytes encoded as "XYZ=" + + byte b1 = (byte) (data >>> 10); + byte b2 = (byte) ((data >>> 2) & 0xFF); + + if (index < end - 1) { + buffer[index++] = b1; + buffer[index++] = b2; + } else if (index < end) { + buffer[index++] = b1; + decodedBuf.append(b2); + } else { + decodedBuf.append(b1); + decodedBuf.append(b2); + } + } else { + // error in encoded data + handleUnexpecedPad(sextets); + } + + return index; + } + + private void handleUnexpectedEof(int sextets) throws IOException { + if (monitor.warn("Unexpected end of BASE64 stream", "dropping " + sextets + " sextet(s)")) + throw new IOException("Unexpected end of BASE64 stream"); + } + + private void handleUnexpecedPad(int sextets) throws IOException { + if (monitor.warn("Unexpected padding character", "dropping " + sextets + " sextet(s)")) + throw new IOException("Unexpected padding character"); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Base64OutputStream.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Base64OutputStream.java new file mode 100644 index 0000000..38f0d46 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Base64OutputStream.java @@ -0,0 +1,174 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Base64; + +/** + * This class implements section 6.8. Base64 Content-Transfer-Encoding + * from RFC 2045 Multipurpose Internet Mail Extensions (MIME) Part One: + * Format of Internet Message Bodies by Freed and Borenstein. + *

+ * Code is based on Base64 and Base64OutputStream code from Commons-Codec 1.4. + * + * @see RFC 2045 + */ +public class Base64OutputStream extends OutputStream { + + // Default line length per RFC 2045 section 6.8. + private static final int DEFAULT_LINE_LENGTH = 76; + + // CRLF line separator per RFC 2045 section 2.1. + private static final byte[] CRLF_SEPARATOR = { '\r', '\n' }; + + // This array is a lookup table that translates 6-bit positive integer index + // values into their "Base64 Alphabet" equivalents as specified in Table 1 + // of RFC 2045. + static final byte[] BASE64_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', '+', '/' }; + + private final OutputStream delegate; + + /** + * Creates a Base64OutputStream that writes the encoded data + * to the given output stream using the default line length (76) and line + * separator (CRLF). + * + * @param out + * underlying output stream. + */ + public Base64OutputStream(OutputStream out) { + this(out, DEFAULT_LINE_LENGTH, CRLF_SEPARATOR); + } + + /** + * Creates a Base64OutputStream that writes the encoded data + * to the given output stream using the given line length and the default + * line separator (CRLF). + *

+ * The given line length will be rounded up to the nearest multiple of 4. If + * the line length is zero then the output will not be split into lines. + * + * @param out + * underlying output stream. + * @param lineLength + * desired line length. + */ + public Base64OutputStream(OutputStream out, int lineLength) { + this(out, lineLength, CRLF_SEPARATOR); + } + + /** + * Creates a Base64OutputStream that writes the encoded data + * to the given output stream using the given line length and line + * separator. + *

+ * The given line length will be rounded up to the nearest multiple of 4. If + * the line length is zero then the output will not be split into lines and + * the line separator is ignored. + *

+ * The line separator must not include characters from the BASE64 alphabet + * (including the padding character =). + * + * @param out + * underlying output stream. + * @param lineLength + * desired line length. + * @param lineSeparator + * line separator to use. + */ + public Base64OutputStream(OutputStream out, int lineLength, byte[] lineSeparator) { + ExtraCrlfOutputStream wrapped = new ExtraCrlfOutputStream(out, lineSeparator); + this.delegate = Base64.getMimeEncoder(lineLength, lineSeparator).wrap(wrapped); + } + + @Override + public void write(int i) throws IOException { + delegate.write(i); + } + + @Override + public void write(byte[] b) throws IOException { + delegate.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + delegate.write(b, off, len); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + private static class ExtraCrlfOutputStream extends OutputStream { + private final OutputStream delegate; + private final byte[] lineSeparator; + private boolean appendExtraCrlf; + + private ExtraCrlfOutputStream(OutputStream delegate, byte[] lineSeparator) { + this.delegate = delegate; + this.lineSeparator = lineSeparator; + this.appendExtraCrlf = false; + } + + @Override + public void write(int i) throws IOException { + delegate.write(i); + appendExtraCrlf = true; + } + + @Override + public void write(byte[] b) throws IOException { + delegate.write(b); + appendExtraCrlf |= b.length > 0; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + delegate.write(b, off, len); + appendExtraCrlf |= len> 0; + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + if (appendExtraCrlf) { + delegate.write(lineSeparator); + } + } + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/BinaryInputStream.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/BinaryInputStream.java new file mode 100644 index 0000000..7920547 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/BinaryInputStream.java @@ -0,0 +1,92 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * {@link InputStream} backed by byte array. + */ +class BinaryInputStream extends InputStream { + + private final ByteBuffer bbuf; + + BinaryInputStream(final ByteBuffer b) { + super(); + this.bbuf = b; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } + if (off < 0 || len < 0 || off + len > b.length) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return 0; + } + if (this.bbuf.hasRemaining()) { + int chunk = Math.min(this.bbuf.remaining(), len); + this.bbuf.get(b, off, chunk); + return chunk; + } else { + return -1; + } + } + + @Override + public int read() throws IOException { + if (this.bbuf.hasRemaining()) { + return this.bbuf.get() & 0xFF; + } else { + return -1; + } + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public long skip(long n) throws IOException { + int skipped = 0; + while (n > 0 && this.bbuf.hasRemaining()) { + this.bbuf.get(); + n--; + skipped++; + } + return skipped; + } + + @Override + public int available() throws IOException { + return this.bbuf.remaining(); + } + + @Override + public void close() throws IOException { + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ByteArrayBuffer.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ByteArrayBuffer.java new file mode 100644 index 0000000..eeadc5c --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ByteArrayBuffer.java @@ -0,0 +1,180 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +/** + * A resizable byte array. + */ +public final class ByteArrayBuffer { + + private byte[] buffer; + private int len; + + public ByteArrayBuffer(int capacity) { + super(); + if (capacity < 0) { + throw new IllegalArgumentException("Buffer capacity may not be negative"); + } + this.buffer = new byte[capacity]; + } + + public ByteArrayBuffer(byte[] bytes, boolean dontCopy) { + this(bytes, bytes.length, dontCopy); + } + + public ByteArrayBuffer(byte[] bytes, int len, boolean dontCopy) { + if (bytes == null) + throw new IllegalArgumentException(); + if (len < 0 || len > bytes.length) + throw new IllegalArgumentException(); + + if (dontCopy) { + this.buffer = bytes; + } else { + this.buffer = new byte[len]; + System.arraycopy(bytes, 0, this.buffer, 0, len); + } + + this.len = len; + } + + private void expand(int newlen) { + byte newbuffer[] = new byte[Math.max(this.buffer.length << 1, newlen)]; + System.arraycopy(this.buffer, 0, newbuffer, 0, this.len); + this.buffer = newbuffer; + } + + public void append(final byte[] b, int off, int len) { + if (b == null) { + return; + } + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) < 0) || ((off + len) > b.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + int newlen = this.len + len; + if (newlen > this.buffer.length) { + expand(newlen); + } + System.arraycopy(b, off, this.buffer, this.len, len); + this.len = newlen; + } + + public void append(int b) { + int newlen = this.len + 1; + if (newlen > this.buffer.length) { + expand(newlen); + } + this.buffer[this.len] = (byte)b; + this.len = newlen; + } + + public void clear() { + this.len = 0; + } + + public byte[] toByteArray() { + byte[] b = new byte[this.len]; + if (this.len > 0) { + System.arraycopy(this.buffer, 0, b, 0, this.len); + } + return b; + } + + public byte byteAt(int i) { + if (i < 0 || i >= this.len) + throw new IndexOutOfBoundsException(); + + return this.buffer[i]; + } + + public int capacity() { + return this.buffer.length; + } + + public int length() { + return this.len; + } + + public byte[] buffer() { + return this.buffer; + } + + public int indexOf(byte b) { + return indexOf(b, 0, this.len); + } + + public int indexOf(byte b, int beginIndex, int endIndex) { + if (beginIndex < 0) { + beginIndex = 0; + } + if (endIndex > this.len) { + endIndex = this.len; + } + if (beginIndex > endIndex) { + return -1; + } + for (int i = beginIndex; i < endIndex; i++) { + if (this.buffer[i] == b) { + return i; + } + } + return -1; + } + + public void setLength(int len) { + if (len < 0 || len > this.buffer.length) { + throw new IndexOutOfBoundsException(); + } + this.len = len; + } + + public void remove(int off, int len) { + if ((off < 0) || (off > this.len) || (len < 0) || + ((off + len) < 0) || ((off + len) > this.len)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + int remaining = this.len - off - len; + if (remaining > 0) { + System.arraycopy(this.buffer, off + len, this.buffer, off, remaining); + } + this.len -= len; + } + + public boolean isEmpty() { + return this.len == 0; + } + + public boolean isFull() { + return this.len == this.buffer.length; + } + + @Override + public String toString() { + return new String(toByteArray()); + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ByteSequencePro.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ByteSequencePro.java new file mode 100644 index 0000000..213a001 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ByteSequencePro.java @@ -0,0 +1,58 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +/** + * An immutable sequence of bytes. + */ +public interface ByteSequencePro { + + /** + * An empty byte sequence. + */ + ByteSequencePro EMPTY = new EmptyByteSequence(); + + /** + * Returns the length of this byte sequence. + * + * @return the number of bytes in this sequence. + */ + int length(); + + /** + * Returns the byte value at the specified index. + * + * @param index + * the index of the byte value to be returned. + * @return the corresponding byte value + * @throws IndexOutOfBoundsException + * if index < 0 || index >= length(). + */ + byte byteAt(int index); + + /** + * Copies the contents of this byte sequence into a newly allocated byte + * array and returns that array. + * + * @return a byte array holding a copy of this byte sequence. + */ + byte[] toByteArray(); + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/CharsetUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/CharsetUtil.java new file mode 100644 index 0000000..cced1f3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/CharsetUtil.java @@ -0,0 +1,141 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; + +/** + * Utility class for working with character sets. + */ +public class CharsetUtil { + + /** carriage return - line feed sequence */ + public static final String CRLF = "\r\n"; + + /** US-ASCII CR, carriage return (13) */ + public static final int CR = '\r'; + + /** US-ASCII LF, line feed (10) */ + public static final int LF = '\n'; + + /** US-ASCII SP, space (32) */ + public static final int SP = ' '; + + /** US-ASCII HT, horizontal-tab (9) */ + public static final int HT = '\t'; + + /** + * Returns true if the specified character falls into the US + * ASCII character set (Unicode range 0000 to 007f). + * + * @param ch + * character to test. + * @return true if the specified character falls into the US + * ASCII character set, false otherwise. + */ + public static boolean isASCII(char ch) { + return (0xFF80 & ch) == 0; + } + + /** + * Returns true if the specified string consists entirely of + * US ASCII characters. + * + * @param s + * string to test. + * @return true if the specified string consists entirely of + * US ASCII characters, false otherwise. + */ + public static boolean isASCII(final String s) { + if (s == null) { + throw new IllegalArgumentException("String may not be null"); + } + final int len = s.length(); + for (int i = 0; i < len; i++) { + if (!isASCII(s.charAt(i))) { + return false; + } + } + return true; + } + + /** + * Returns true if the specified character is a whitespace + * character (CR, LF, SP or HT). + * + * @param ch + * character to test. + * @return true if the specified character is a whitespace + * character, false otherwise. + */ + public static boolean isWhitespace(char ch) { + return ch == SP || ch == HT || ch == CR || ch == LF; + } + + /** + * Returns true if the specified string consists entirely of + * whitespace characters. + * + * @param s + * string to test. + * @return true if the specified string consists entirely of + * whitespace characters, false otherwise. + */ + public static boolean isWhitespace(final String s) { + if (s == null) { + throw new IllegalArgumentException("String may not be null"); + } + final int len = s.length(); + for (int i = 0; i < len; i++) { + if (!isWhitespace(s.charAt(i))) { + return false; + } + } + return true; + } + + /** + *

+ * Returns a {@link Charset} instance if character set with the given name + * is recognized and supported by Java runtime. Returns null + * otherwise. + *

+ *

+ * This method is a wrapper around {@link Charset#forName(String)} method + * that catches {@link IllegalCharsetNameException} and + * {@link UnsupportedCharsetException} and returns null. + *

+ */ + public static Charset lookup(final String name) { + if (name == null) { + return null; + } + try { + return Charset.forName(name); + } catch (IllegalCharsetNameException ex) { + return null; + } catch (UnsupportedCharsetException ex) { + return null; + } + } + + } diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Charsets.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Charsets.java new file mode 100644 index 0000000..39bb6cc --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Charsets.java @@ -0,0 +1,32 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.nio.charset.Charset; + +public final class Charsets { + + public static final Charset US_ASCII = Charset.forName("US-ASCII"); + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + public static final Charset DEFAULT_CHARSET = US_ASCII; + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionField.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionField.java new file mode 100644 index 0000000..3312842 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionField.java @@ -0,0 +1,156 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.util.Date; +import java.util.Map; + +public interface ContentDispositionField { + + /** The inline disposition type. */ + String DISPOSITION_TYPE_INLINE = "inline"; + /** The attachment disposition type. */ + String DISPOSITION_TYPE_ATTACHMENT = "attachment"; + /** The name of the filename parameter. */ + String PARAM_FILENAME = "filename"; + /** The name of the creation-date parameter. */ + String PARAM_CREATION_DATE = "creation-date"; + /** The name of the modification-date parameter. */ + String PARAM_MODIFICATION_DATE = "modification-date"; + /** The name of the read-date parameter. */ + String PARAM_READ_DATE = "read-date"; + /** The name of the size parameter. */ + String PARAM_SIZE = "size"; + + /** + * Returns true if this field is valid, i.e. no errors were + * encountered while parsing the field value. + * + * @return true if this field is valid, false + * otherwise. + * @see #getParseException() + */ + boolean isValidField(); + + /** + * Returns the exception that was thrown by the field parser while parsing + * the field value. The result is null if the field is valid + * and no errors were encountered. + * + * @return the exception that was thrown by the field parser or + * null if the field is valid. + */ + ParseException getParseException(); + + /** + * Gets the disposition type defined in this Content-Disposition field. + * + * @return the disposition type or an empty string if not set. + */ + String getDispositionType(); + + /** + * Gets the value of a parameter. Parameter names are case-insensitive. + * + * @param name + * the name of the parameter to get. + * @return the parameter value or null if not set. + */ + String getParameter(String name); + + /** + * Gets all parameters. + * + * @return the parameters. + */ + Map getParameters(); + + /** + * Determines if the disposition type of this field matches the given one. + * + * @param dispositionType + * the disposition type to match against. + * @return true if the disposition type of this field + * matches, false otherwise. + */ + boolean isDispositionType(String dispositionType); + + /** + * Return true if the disposition type of this field is + * inline, false otherwise. + * + * @return true if the disposition type of this field is + * inline, false otherwise. + */ + boolean isInline(); + + /** + * Return true if the disposition type of this field is + * attachment, false otherwise. + * + * @return true if the disposition type of this field is + * attachment, false otherwise. + */ + boolean isAttachment(); + + /** + * Gets the value of the filename parameter if set. + * + * @return the filename parameter value or null + * if not set. + */ + String getFilename(); + + /** + * Gets the value of the creation-date parameter if set and + * valid. + * + * @return the creation-date parameter value or + * null if not set or invalid. + */ + Date getCreationDate(); + + /** + * Gets the value of the modification-date parameter if set + * and valid. + * + * @return the modification-date parameter value or + * null if not set or invalid. + */ + Date getModificationDate(); + + /** + * Gets the value of the read-date parameter if set and + * valid. + * + * @return the read-date parameter value or null + * if not set or invalid. + */ + Date getReadDate(); + + /** + * Gets the value of the size parameter if set and valid. + * + * @return the size parameter value or -1 if + * not set or invalid. + */ + long getSize(); + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionHolder.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionHolder.java new file mode 100644 index 0000000..d12a83c --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionHolder.java @@ -0,0 +1,136 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import static org.mozilla.xiu.browser.utils.contentdisposition.ContentDispositionField.DISPOSITION_TYPE_ATTACHMENT; +import static org.mozilla.xiu.browser.utils.contentdisposition.ContentDispositionField.DISPOSITION_TYPE_INLINE; +import static org.mozilla.xiu.browser.utils.contentdisposition.ContentDispositionField.PARAM_FILENAME; +import static org.mozilla.xiu.browser.utils.contentdisposition.ContentDispositionField.PARAM_SIZE; + +import java.io.StringReader; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Represents a Content-Disposition field. + */ +public class ContentDispositionHolder { + + private String body; + + private boolean parsed = false; + + private String dispositionType = ""; + private final Map parameters = new HashMap<>(); + private ParseException parseException; + + public ContentDispositionHolder(String body) { + this.body = body; + } + + public ParseException getParseException() { + if (!parsed) + parse(); + + return parseException; + } + + public String getDispositionType() { + if (!parsed) + parse(); + + return dispositionType; + } + + public String getParameter(String name) { + if (!parsed) + parse(); + + return parameters.get(name.toLowerCase()); + } + + public Map getParameters() { + if (!parsed) + parse(); + + return Collections.unmodifiableMap(parameters); + } + + public boolean isDispositionType(String dispositionType) { + if (!parsed) + parse(); + + return this.dispositionType.equalsIgnoreCase(dispositionType); + } + + public boolean isInline() { + if (!parsed) + parse(); + + return dispositionType.equals(DISPOSITION_TYPE_INLINE); + } + + public boolean isAttachment() { + if (!parsed) + parse(); + + return dispositionType.equals(DISPOSITION_TYPE_ATTACHMENT); + } + + public String getFilename() { + return getParameter(PARAM_FILENAME); + } + + public long getSize() { + String value = getParameter(PARAM_SIZE); + if (value == null) + return -1; + + try { + long size = Long.parseLong(value); + return size < 0 ? -1 : size; + } catch (NumberFormatException e) { + return -1; + } + } + + private void parse() { + ContentDispositionParser parser = new ContentDispositionParser( + new StringReader(body)); + try { + parser.parseAll(); + } catch (ParseException e) { + parseException = e; + } catch (TokenMgrError e) { + parseException = new ParseException(e); + } + + final String dispositionType = parser.getDispositionType(); + + if (dispositionType != null) { + this.dispositionType = dispositionType.toLowerCase(Locale.US); + this.parameters.putAll(parser.getParameters()); + } + parsed = true; + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionParser.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionParser.java new file mode 100644 index 0000000..8e0a347 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionParser.java @@ -0,0 +1,286 @@ +/* Generated By:JavaCC: Do not edit this line. ContentDispositionParser.java */ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.util.Map; + +public class ContentDispositionParser implements ContentDispositionParserConstants { + + private String dispositionType; + + private final MimeParameterMapping mapping = new MimeParameterMapping(); + + public String getDispositionType() { + return dispositionType; + } + + public Map getParameters() { + return mapping.getParameters(); + } + + public static void main(String args[]) throws ParseException { + while (true) { + try { + ContentDispositionParser parser = new ContentDispositionParser( + System.in); + parser.parseLine(); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + final public void parseLine() throws ParseException { + parse(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 1: + jj_consume_token(1); + break; + default: + jj_la1[0] = jj_gen; + ; + } + jj_consume_token(2); + } + + final public void parseAll() throws ParseException { + parse(); + jj_consume_token(0); + } + + final public void parse() throws ParseException { + Token dispositionType; + dispositionType = jj_consume_token(ATOKEN); + this.dispositionType = dispositionType.image; + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + ; + break; + default: + jj_la1[1] = jj_gen; + break label_1; + } + jj_consume_token(3); + parameter(); + } + } + + final public void parameter() throws ParseException { + Token attrib; + String val; + attrib = jj_consume_token(ATOKEN); + jj_consume_token(4); + val = value(); + mapping.addParameter(attrib.image, val); + } + + final public String value() throws ParseException { + Token t; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ATOKEN: + t = jj_consume_token(ATOKEN); + break; + case DIGITS: + t = jj_consume_token(DIGITS); + break; + case QUOTEDSTRING: + t = jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return t.image;} + throw new Error("Missing return statement in function"); + } + + /** Generated Token Manager. */ + public ContentDispositionParserTokenManager token_source; + SimpleCharStream jj_input_stream; + /** Current token. */ + public Token token; + /** Next token. */ + public Token jj_nt; + private int jj_ntk; + private int jj_gen; + final private int[] jj_la1 = new int[3]; + static private int[] jj_la1_0; + static { + jj_la1_init_0(); + } + private static void jj_la1_init_0() { + jj_la1_0 = new int[] {0x2,0x8,0x1c0000,}; + } + + /** Constructor with InputStream. */ + public ContentDispositionParser(java.io.InputStream stream) { + this(stream, null); + } + /** Constructor with InputStream and supplied encoding */ + public ContentDispositionParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new ContentDispositionParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + /** Reinitialise. */ + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + /** Constructor. */ + public ContentDispositionParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new ContentDispositionParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + /** Constructor with generated Token Manager. */ + public ContentDispositionParser(ContentDispositionParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + /** Reinitialise. */ + public void ReInit(ContentDispositionParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + +/** Get the next Token. */ + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + +/** Get the specific Token. */ + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.List jj_expentries = new java.util.ArrayList(); + private int[] jj_expentry; + private int jj_kind = -1; + + /** Generate ParseException. */ + public ParseException generateParseException() { + jj_expentries.clear(); + boolean[] la1tokens = new boolean[23]; + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 3; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1<", + "\"\\r\"", + "\"\\n\"", + "\";\"", + "\"=\"", + "", + "\"(\"", + "\")\"", + "", + "\"(\"", + "", + "", + "\"(\"", + "\")\"", + "", + "\"\\\"\"", + "", + "", + "\"\\\"\"", + "", + "", + "", + "", + }; + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionParserTokenManager.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionParserTokenManager.java new file mode 100644 index 0000000..144d4a1 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ContentDispositionParserTokenManager.java @@ -0,0 +1,855 @@ +/* Generated By:JavaCC: Do not edit this line. ContentDispositionParserTokenManager.java */ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +/** Token Manager. */ +public class ContentDispositionParserTokenManager implements ContentDispositionParserConstants +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; + + /** Debug output. */ + public java.io.PrintStream debugStream = System.out; + /** Set debug output. */ + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0) +{ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 10: + return jjStartNfaWithStates_0(0, 2, 2); + case 13: + return jjStartNfaWithStates_0(0, 1, 2); + case 34: + return jjStopAtPos(0, 15); + case 40: + return jjStopAtPos(0, 6); + case 59: + return jjStopAtPos(0, 3); + case 61: + return jjStopAtPos(0, 4); + default : + return jjMoveNfa_0(3, 0); + } +} +private int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +static final long[] jjbitVec0 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private int jjMoveNfa_0(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 3: + if ((0x3ff6cfafffffdffL & l) != 0L) + { + if (kind > 20) + kind = 20; + jjCheckNAdd(2); + } + else if ((0x100000200L & l) != 0L) + { + if (kind > 5) + kind = 5; + jjCheckNAdd(0); + } + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 19) + kind = 19; + jjCheckNAdd(1); + } + break; + case 0: + if ((0x100000200L & l) == 0L) + break; + kind = 5; + jjCheckNAdd(0); + break; + case 1: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 19) + kind = 19; + jjCheckNAdd(1); + break; + case 2: + if ((0x3ff6cfafffffdffL & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 3: + case 2: + if ((0xffffffffc7fffffeL & l) == 0L) + break; + kind = 20; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 3: + case 2: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 9); + case 41: + return jjStopAtPos(0, 7); + default : + return jjMoveNfa_1(0, 0); + } +} +private int jjMoveNfa_1(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 10) + kind = 10; + break; + case 1: + if (kind > 8) + kind = 8; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 10) + kind = 10; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 8) + kind = 8; + break; + case 2: + if (kind > 10) + kind = 10; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 10) + kind = 10; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 8) + kind = 8; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_3(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_3(int pos, long active0) +{ + return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1); +} +private int jjMoveStringLiteralDfa0_3() +{ + switch(curChar) + { + case 34: + return jjStopAtPos(0, 18); + default : + return jjMoveNfa_3(0, 0); + } +} +private int jjMoveNfa_3(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((0xfffffffbffffffffL & l) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAdd(2); + break; + case 1: + if (kind > 16) + kind = 16; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffffefffffffL & l) != 0L) + { + if (kind > 17) + kind = 17; + jjCheckNAdd(2); + } + else if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 16) + kind = 16; + break; + case 2: + if ((0xffffffffefffffffL & l) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 17) + kind = 17; + jjCheckNAdd(2); + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 16) + kind = 16; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 12); + case 41: + return jjStopAtPos(0, 13); + default : + return jjMoveNfa_2(0, 0); + } +} +private int jjMoveNfa_2(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 14) + kind = 14; + break; + case 1: + if (kind > 11) + kind = 11; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 14) + kind = 14; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 11) + kind = 11; + break; + case 2: + if (kind > 14) + kind = 14; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 14) + kind = 14; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 11) + kind = 11; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { +}; + +/** Token literal values. */ +public static final String[] jjstrLiteralImages = { +"", "\15", "\12", "\73", "\75", null, null, null, null, null, null, null, null, +null, null, null, null, null, null, null, null, null, null, }; + +/** Lexer state names. */ +public static final String[] lexStateNames = { + "DEFAULT", + "INCOMMENT", + "NESTED_COMMENT", + "INQUOTEDSTRING", +}; + +/** Lex State array. */ +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, 1, 0, -1, 2, -1, -1, -1, -1, -1, 3, -1, -1, 0, -1, -1, -1, -1, +}; +static final long[] jjtoToken = { + 0x1c001fL, +}; +static final long[] jjtoSkip = { + 0xa0L, +}; +static final long[] jjtoSpecial = { + 0x20L, +}; +static final long[] jjtoMore = { + 0x3ff40L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[3]; +private final int[] jjstateSet = new int[6]; +private final StringBuilder jjimage = new StringBuilder(); +private StringBuilder image = jjimage; +private int jjimageLen; +private int lengthOfMatch; +protected char curChar; +/** Constructor. */ +public ContentDispositionParserTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} + +/** Constructor. */ +public ContentDispositionParserTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} + +/** Reinitialise parser. */ +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 3; i-- > 0;) + jjrounds[i] = 0x80000000; +} + +/** Reinitialise parser. */ +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} + +/** Switch to specified lex state. */ +public void SwitchTo(int lexState) +{ + if (lexState >= 4 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + final Token t; + final String curTokenImage; + final int beginLine; + final int endLine; + final int beginColumn; + final int endColumn; + String im = jjstrLiteralImages[jjmatchedKind]; + curTokenImage = (im == null) ? input_stream.GetImage() : im; + beginLine = input_stream.getBeginLine(); + beginColumn = input_stream.getBeginColumn(); + endLine = input_stream.getEndLine(); + endColumn = input_stream.getEndColumn(); + t = Token.newToken(jjmatchedKind, curTokenImage); + + t.beginLine = beginLine; + t.endLine = endLine; + t.beginColumn = beginColumn; + t.endColumn = endColumn; + + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +/** Get the next Token. */ +public Token getNextToken() +{ + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + image = jjimage; + image.setLength(0); + jjimageLen = 0; + + for (;;) + { + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + case 3: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_3(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + TokenLexicalActions(matchedToken); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + } + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + MoreLexicalActions(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + curPos = 0; + jjmatchedKind = 0x7fffffff; + try { + curChar = input_stream.readChar(); + continue; + } + catch (java.io.IOException e1) { } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } + } +} + +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + case 8 : + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 9 : + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + commentNest = 1; + break; + case 11 : + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 12 : + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + ++commentNest; + break; + case 13 : + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); + break; + case 15 : + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 1); + break; + case 16 : + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + default : + break; + } +} +void TokenLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + case 18 : + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + matchedToken.image = image.substring(0, image.length() - 1); + break; + default : + break; + } +} +private void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/DecodeMonitor.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/DecodeMonitor.java new file mode 100644 index 0000000..88bd6b6 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/DecodeMonitor.java @@ -0,0 +1,63 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +/** + * This class is used to drive how decoder/parser should deal with malformed + * and unexpected data. + * + * 2 basic implementations are provided: + *
    + *
  • {@link #STRICT} return "true" on any occurrence
  • + *
  • {@link #SILENT} ignores any problem
  • + *
+ */ +public class DecodeMonitor { + + /** + * The STRICT monitor throws an exception on every event. + */ + public static final DecodeMonitor STRICT = new DecodeMonitor() { + + @Override + public boolean warn(String error, String dropDesc) { + return true; + } + + @Override + public boolean isListening() { + return true; + } + }; + + /** + * The SILENT monitor ignore requests. + */ + public static final DecodeMonitor SILENT = new DecodeMonitor(); + + public boolean warn(String error, String dropDesc) { + return false; + } + + public boolean isListening() { + return false; + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/DecoderUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/DecoderUtil.java new file mode 100644 index 0000000..099b861 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/DecoderUtil.java @@ -0,0 +1,335 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Map; + +/** + * Static methods for decoding strings, byte arrays and encoded words. + */ +public class DecoderUtil { + + /** + * Decodes a string containing quoted-printable encoded data. + * + * @param s the string to decode. + * @return the decoded bytes. + */ + private static byte[] decodeQuotedPrintable(String s, DecodeMonitor monitor) { + try { + QuotedPrintableInputStream is = new QuotedPrintableInputStream( + InputStreams.createAscii(s), monitor); + try { + ByteArrayBuffer buf = new ByteArrayBuffer(s.length()); + int b; + while ((b = is.read()) != -1) { + buf.append(b); + } + return buf.toByteArray(); + } finally { + is.close(); + } + } catch (IOException ex) { + // This should never happen! + throw new Error(ex); + } + } + + /** + * Decodes a string containing base64 encoded data. + * + * @param s the string to decode. + * @param monitor + * @return the decoded bytes. + */ + private static byte[] decodeBase64(String s, DecodeMonitor monitor) { + try { + Base64InputStream is = new Base64InputStream( + InputStreams.createAscii(s), monitor); + try { + ByteArrayBuffer buf = new ByteArrayBuffer(s.length()); + int b; + while ((b = is.read()) != -1) { + buf.append(b); + } + return buf.toByteArray(); + } finally { + is.close(); + } + } catch (IOException ex) { + // This should never happen! + throw new Error(ex); + } + } + + /** + * Decodes an encoded text encoded with the 'B' encoding (described in + * RFC 2047) found in a header field body. + * + * @param encodedText the encoded text to decode. + * @param charset the Java charset to use. + * @param monitor + * @return the decoded string. + * @throws UnsupportedEncodingException if the given Java charset isn't + * supported. + */ + static String decodeB(String encodedText, String charset, DecodeMonitor monitor) + throws UnsupportedEncodingException { + byte[] decodedBytes = decodeBase64(encodedText, monitor); + return new String(decodedBytes, charset); + } + + /** + * Decodes an encoded text encoded with the 'Q' encoding (described in + * RFC 2047) found in a header field body. + * + * @param encodedText the encoded text to decode. + * @param charset the Java charset to use. + * @return the decoded string. + * @throws UnsupportedEncodingException if the given Java charset isn't + * supported. + */ + static String decodeQ(String encodedText, String charset, DecodeMonitor monitor) + throws UnsupportedEncodingException { + encodedText = replaceUnderscores(encodedText); + + byte[] decodedBytes = decodeQuotedPrintable(encodedText, monitor); + return new String(decodedBytes, charset); + } + + static String decodeEncodedWords(String body) { + return decodeEncodedWords(body, DecodeMonitor.SILENT); + } + + /** + * Decodes a string containing encoded words as defined by RFC 2047. Encoded + * words have the form =?charset?enc?encoded-text?= where enc is either 'Q' + * or 'q' for quoted-printable and 'B' or 'b' for base64. + * + * @param body the string to decode + * @param monitor the DecodeMonitor to be used. + * @return the decoded string. + * @throws IllegalArgumentException only if the DecodeMonitor strategy throws it (Strict parsing) + */ + public static String decodeEncodedWords(String body, DecodeMonitor monitor) throws IllegalArgumentException { + return decodeEncodedWords(body, monitor, null, Collections.emptyMap()); + } + + /** + * Decodes a string containing encoded words as defined by RFC 2047. Encoded + * words have the form =?charset?enc?encoded-text?= where enc is either 'Q' + * or 'q' for quoted-printable and 'B' or 'b' for base64. Using fallback + * charset if charset in encoded words is invalid. + * + * @param body the string to decode + * @param fallback the fallback Charset to be used. + * @return the decoded string. + * @throws IllegalArgumentException only if the DecodeMonitor strategy throws it (Strict parsing) + */ + public static String decodeEncodedWords(String body, Charset fallback) throws IllegalArgumentException { + return decodeEncodedWords(body, null, fallback, Collections.emptyMap()); + } + + /** + * Decodes a string containing encoded words as defined by RFC 2047. Encoded + * words have the form =?charset?enc?encoded-text?= where enc is either 'Q' + * or 'q' for quoted-printable and 'B' or 'b' for base64. Using fallback + * charset if charset in encoded words is invalid. + * + * @param body the string to decode + * @param monitor the DecodeMonitor to be used. + * @param fallback the fallback Charset to be used. + * @return the decoded string. + * @throws IllegalArgumentException only if the DecodeMonitor strategy throws it (Strict parsing) + */ + public static String decodeEncodedWords(String body, DecodeMonitor monitor, Charset fallback) + throws IllegalArgumentException { + return decodeEncodedWords(body, monitor, fallback, Collections.emptyMap()); + } + + /** + * Decodes a string containing encoded words as defined by RFC 2047. Encoded + * words have the form =?charset?enc?encoded-text?= where enc is either 'Q' + * or 'q' for quoted-printable and 'B' or 'b' for base64. Using fallback + * charset if charset in encoded words is invalid. Additionally, the found charset + * will be overridden if a corresponding mapping is found. + * + * @param body the string to decode + * @param monitor the DecodeMonitor to be used. + * @param fallback the fallback Charset to be used. + * @param charsetOverrides the Charsets to override and their replacements. Must not be null. + * @return the decoded string. + * @throws IllegalArgumentException only if the DecodeMonitor strategy throws it (Strict parsing) + */ + public static String decodeEncodedWords(String body, DecodeMonitor monitor, Charset fallback, + Map charsetOverrides) + throws IllegalArgumentException { + + StringBuilder sb = new StringBuilder(); + int position = 0; + + while (position < body.length()) { + int startPattern = body.indexOf("=?", position); + if (startPattern < 0) { + if (position == 0) { + return body; + } + sb.append(body, position, body.length()); + break; + } + + int charsetEnd = body.indexOf('?', startPattern + 2); + int encodingEnd = body.indexOf('?', charsetEnd + 1); + int encodedTextEnd = body.indexOf("?=", encodingEnd + 1); + + if (charsetEnd < 0 || encodingEnd < 0 || encodedTextEnd < 0) { + // Invalid pattern + sb.append(body, position, startPattern + 2); + position = startPattern + 2; + } else if (encodingEnd == encodedTextEnd) { + sb.append(body, position, Math.min(encodedTextEnd + 2, body.length())); + position = encodedTextEnd +2; + } else { + String separator = body.substring(position, startPattern); + if ((!CharsetUtil.isWhitespace(separator) || position == 0) && !separator.isEmpty()) { + sb.append(separator); + } + String mimeCharset = body.substring(startPattern + 2, charsetEnd); + String encoding = body.substring(charsetEnd + 1, encodingEnd); + String encodedText = body.substring(encodingEnd + 1, encodedTextEnd); + + if (encodedText.isEmpty()) { + position = encodedTextEnd + 2; + continue; + } + String decoded; + decoded = tryDecodeEncodedWord(mimeCharset, encoding, encodedText, monitor, fallback, charsetOverrides); + if (decoded != null) { + if (!CharsetUtil.isWhitespace(decoded) && !decoded.isEmpty()) { + sb.append(decoded); + } + } else { + sb.append(body, startPattern, encodedTextEnd + 2); + } + position = encodedTextEnd + 2; + } + } + return sb.toString(); + } + + // return null on error + private static String tryDecodeEncodedWord( + final String mimeCharset, + final String encoding, + final String encodedText, + final DecodeMonitor monitor, + final Charset fallback, + final Map charsetOverrides) { + Charset charset = lookupCharset(mimeCharset, fallback, charsetOverrides); + if (charset == null) { + monitor(monitor, mimeCharset, encoding, encodedText, "leaving word encoded", + "Mime charser '", mimeCharset, "' doesn't have a corresponding Java charset"); + return null; + } + + if (encodedText.length() == 0) { + monitor(monitor, mimeCharset, encoding, encodedText, "leaving word encoded", + "Missing encoded text in encoded word"); + return null; + } + + try { + if (encoding.equalsIgnoreCase("Q")) { + return DecoderUtil.decodeQ(encodedText, charset.name(), monitor); + } else if (encoding.equalsIgnoreCase("B")) { + return DecoderUtil.decodeB(encodedText, charset.name(), monitor); + } else { + monitor(monitor, mimeCharset, encoding, encodedText, "leaving word encoded", + "Warning: Unknown encoding in encoded word"); + return null; + } + } catch (UnsupportedEncodingException e) { + // should not happen because of isDecodingSupported check above + monitor(monitor, mimeCharset, encoding, encodedText, "leaving word encoded", + "Unsupported encoding (", e.getMessage(), ") in encoded word"); + return null; + } catch (RuntimeException e) { + monitor(monitor, mimeCharset, encoding, encodedText, "leaving word encoded", + "Could not decode (", e.getMessage(), ") encoded word"); + return null; + } + } + + private static Charset lookupCharset( + final String mimeCharset, + final Charset fallback, + final Map charsetOverrides) { + Charset charset = CharsetUtil.lookup(mimeCharset); + if (charset == null) { + return fallback; + } + Charset override = charsetOverrides.get(charset); + return override != null ? override : charset; + } + + private static void monitor(DecodeMonitor monitor, String mimeCharset, String encoding, + String encodedText, String dropDesc, String... strings) throws IllegalArgumentException { + if (monitor.isListening()) { + String encodedWord = recombine(mimeCharset, encoding, encodedText); + StringBuilder text = new StringBuilder(); + for (String str : strings) { + text.append(str); + } + text.append(" ("); + text.append(encodedWord); + text.append(")"); + String exceptionDesc = text.toString(); + if (monitor.warn(exceptionDesc, dropDesc)) + throw new IllegalArgumentException(text.toString()); + } + } + + private static String recombine(final String mimeCharset, + final String encoding, final String encodedText) { + return "=?" + mimeCharset + "?" + encoding + "?" + encodedText + "?="; + } + + // Replace _ with =20 + private static String replaceUnderscores(String str) { + // probably faster than String#replace(CharSequence, CharSequence) + + StringBuilder sb = new StringBuilder(128); + + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == '_') { + sb.append("=20"); + } else { + sb.append(c); + } + } + + return sb.toString(); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/EmptyByteSequence.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/EmptyByteSequence.java new file mode 100644 index 0000000..e11a212 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/EmptyByteSequence.java @@ -0,0 +1,36 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +final class EmptyByteSequence implements ByteSequencePro { + private static final byte[] EMPTY_BYTES = {}; + + public int length() { + return 0; + } + + public byte byteAt(int index) { + throw new IndexOutOfBoundsException(); + } + + public byte[] toByteArray() { + return EMPTY_BYTES; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/InputStreams.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/InputStreams.java new file mode 100644 index 0000000..ee668c3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/InputStreams.java @@ -0,0 +1,77 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * Factory methods for {@link InputStream} instances backed by binary or textual data that attempt + * to minimize intermediate copying while streaming data. + */ +public final class InputStreams { + + private InputStreams() { + } + + public static InputStream create(final byte[] b, int off, int len) { + if (b == null) { + throw new IllegalArgumentException("Byte array may not be null"); + } + return new BinaryInputStream(ByteBuffer.wrap(b, off, len)); + } + + public static InputStream create(final byte[] b) { + if (b == null) { + throw new IllegalArgumentException("Byte array may not be null"); + } + return new BinaryInputStream(ByteBuffer.wrap(b)); + } + + public static InputStream create(final ByteArrayBuffer b) { + if (b == null) { + throw new IllegalArgumentException("Byte array may not be null"); + } + return new BinaryInputStream(ByteBuffer.wrap(b.buffer(), 0, b.length())); + } + + public static InputStream create(final ByteBuffer b) { + if (b == null) { + throw new IllegalArgumentException("Byte array may not be null"); + } + return new BinaryInputStream(b); + } + + public static InputStream createAscii(final CharSequence s) { + if (s == null) { + throw new IllegalArgumentException("CharSequence may not be null"); + } + return new TextInputStream(s, Charsets.US_ASCII, 1024); + } + + public static InputStream create(final CharSequence s, final Charset charset) { + if (s == null) { + throw new IllegalArgumentException("CharSequence may not be null"); + } + return new TextInputStream(s, charset != null ? charset : Charsets.DEFAULT_CHARSET, 1024); + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/MimeParameterMapping.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/MimeParameterMapping.java new file mode 100644 index 0000000..5c494d3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/MimeParameterMapping.java @@ -0,0 +1,70 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; + +public class MimeParameterMapping { + + private final Map parameters = new HashMap<>(); + + public Map getParameters() { + Map result = new HashMap<>(); + for (Map.Entry entry : parameters.entrySet()) { + result.put(entry.getKey(), decodeParameterValue(entry.getValue()) ); + } + return result; + } + + public void addParameter(String name, String value) { + String key = removeSectionFromName(name).toLowerCase(); + if (parameters.containsKey(key)) { + parameters.put(key, parameters.get(key) + value); + } else { + parameters.put(key, value); + } + } + + private String decodeParameterValue(String value) { + if (value == null) { + return null; + } + int charsetEnd = value.indexOf("'"); + int languageEnd = value.indexOf("'", charsetEnd + 1); + if (charsetEnd < 0 || languageEnd < 0) { + return MimeUtil.unscrambleHeaderValue(value); + } + String charset = value.substring(0, charsetEnd); + String fileName = value.substring(languageEnd + 1); + try { + return java.net.URLDecoder.decode(fileName, charset); + } + catch (UnsupportedEncodingException ignore) { + } + return MimeUtil.unscrambleHeaderValue(value); + } + + private String removeSectionFromName(String parameterName) { + int position = parameterName.indexOf('*'); + return parameterName.substring(0, position < 0 ? parameterName.length() : position); + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/MimeUtil.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/MimeUtil.java new file mode 100644 index 0000000..d095203 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/MimeUtil.java @@ -0,0 +1,265 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.util.Random; + +/** + * A utility class, which provides some MIME related application logic. + */ +public final class MimeUtil { + + /** + * The quoted-printable encoding. + */ + public static final String ENC_QUOTED_PRINTABLE = "quoted-printable"; + /** + * The binary encoding. + */ + public static final String ENC_BINARY = "binary"; + /** + * The base64 encoding. + */ + public static final String ENC_BASE64 = "base64"; + /** + * The 8bit encoding. + */ + public static final String ENC_8BIT = "8bit"; + /** + * The 7bit encoding. + */ + public static final String ENC_7BIT = "7bit"; + + // used to create unique ids + private static final Random random = new Random(); + + // used to create unique ids + private static int counter = 0; + + private MimeUtil() { + // this is an utility class to be used statically. + // this constructor protect from instantiation. + } + + /** + * Returns, whether the given two MIME types are identical. + */ + public static boolean isSameMimeType(String pType1, String pType2) { + return pType1 != null && pType2 != null && pType1.equalsIgnoreCase(pType2); + } + + /** + * Returns true, if the given MIME type is that of a message. + */ + public static boolean isMessage(String pMimeType) { + return pMimeType != null && pMimeType.equalsIgnoreCase("message/rfc822"); + } + + /** + * Return true, if the given MIME type indicates a multipart entity. + */ + public static boolean isMultipart(String pMimeType) { + return pMimeType != null && pMimeType.toLowerCase().startsWith("multipart/"); + } + + /** + * Returns, whether the given transfer-encoding is "base64". + */ + public static boolean isBase64Encoding(String pTransferEncoding) { + return ENC_BASE64.equalsIgnoreCase(pTransferEncoding); + } + + /** + * Returns, whether the given transfer-encoding is "quoted-printable". + */ + public static boolean isQuotedPrintableEncoded(String pTransferEncoding) { + return ENC_QUOTED_PRINTABLE.equalsIgnoreCase(pTransferEncoding); + } + + /** + * Creates a new unique message boundary string that can be used as boundary + * parameter for the Content-Type header field of a message. + * + * @return a new unique message boundary string. + */ + /* TODO - From rfc2045: + * Since the hyphen character ("-") may be represented as itself in the + * Quoted-Printable encoding, care must be taken, when encapsulating a + * quoted-printable encoded body inside one or more multipart entities, + * to ensure that the boundary delimiter does not appear anywhere in the + * encoded body. (A good strategy is to choose a boundary that includes + * a character sequence such as "=_" which can never appear in a + * quoted-printable body. See the definition of multipart messages in + * RFC 2046.) + */ + public static String createUniqueBoundary() { + StringBuilder sb = new StringBuilder(); + sb.append("-=Part."); + sb.append(Integer.toHexString(nextCounterValue())); + sb.append('.'); + sb.append(Long.toHexString(random.nextLong())); + sb.append('.'); + sb.append(Long.toHexString(System.currentTimeMillis())); + sb.append('.'); + sb.append(Long.toHexString(random.nextLong())); + sb.append("=-"); + return sb.toString(); + } + + /** + * Creates a new unique message identifier that can be used in message + * header field such as Message-ID or In-Reply-To. If the given host name is + * not null it will be used as suffix for the message ID + * (following an at sign). + * + * The resulting string is enclosed in angle brackets (< and >); + * + * @param hostName host name to be included in the message ID or + * null if no host name should be included. + * @return a new unique message identifier. + */ + public static String createUniqueMessageId(String hostName) { + StringBuilder sb = new StringBuilder("'); + return sb.toString(); + } + + + /** + * Splits the specified string into a multiple-line representation with + * lines no longer than 76 characters (because the line might contain + * encoded words; see RFC + * 2047 section 2). If the string contains non-whitespace sequences + * longer than 76 characters a line break is inserted at the whitespace + * character following the sequence resulting in a line longer than 76 + * characters. + * + * @param s + * string to split. + * @param usedCharacters + * number of characters already used up. Usually the number of + * characters for header field name plus colon and one space. + * @return a multiple-line representation of the given string. + */ + public static String fold(String s, int usedCharacters) { + final int maxCharacters = 76; + + final int length = s.length(); + if (usedCharacters + length <= maxCharacters) + return s; + + StringBuilder sb = new StringBuilder(); + + int lastLineBreak = -usedCharacters; + int wspIdx = indexOfWsp(s, 0); + while (true) { + if (wspIdx == length) { + sb.append(s.substring(Math.max(0, lastLineBreak))); + return sb.toString(); + } + + int nextWspIdx = indexOfWsp(s, wspIdx + 1); + + if (nextWspIdx - lastLineBreak > maxCharacters) { + sb.append(s, Math.max(0, lastLineBreak), wspIdx); + sb.append("\r\n"); + lastLineBreak = wspIdx; + } + + wspIdx = nextWspIdx; + } + } + + /** + * Unfold a multiple-line representation into a single line. + * + * @param s + * string to unfold. + * @return unfolded string. + */ + public static String unfold(String s) { + final int length = s.length(); + for (int idx = 0; idx < length; idx++) { + char c = s.charAt(idx); + if (c == '\r' || c == '\n') { + return unfold0(s, idx); + } + } + + return s; + } + + /** + Unfold and decode header value + */ + public static String unscrambleHeaderValue(String headerValue) { + return DecoderUtil.decodeEncodedWords( + MimeUtil.unfold(headerValue), + DecodeMonitor.SILENT); + } + + private static String unfold0(String s, int crlfIdx) { + final int length = s.length(); + StringBuilder sb = new StringBuilder(length); + + if (crlfIdx > 0) { + sb.append(s, 0, crlfIdx); + } + + int lastLineBreak = crlfIdx; + for (int idx = crlfIdx + 1; idx < length; idx++) { + char c = s.charAt(idx); + if (c == '\r' || c == '\n') { + if (idx > lastLineBreak + 1) { + sb.append(s, lastLineBreak + 1, idx); + } + lastLineBreak = idx; + } + } + if (lastLineBreak < s.length() - 1 && s.length() > 0) { + sb.append(s, lastLineBreak + 1, s.length()); + } + + return sb.toString(); + } + + private static int indexOfWsp(String s, int fromIndex) { + final int len = s.length(); + for (int index = fromIndex; index < len; index++) { + char c = s.charAt(index); + if (c == ' ' || c == '\t') + return index; + } + return len; + } + + private static synchronized int nextCounterValue() { + return counter++; + } +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ParseException.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ParseException.java new file mode 100644 index 0000000..f731cf7 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/ParseException.java @@ -0,0 +1,235 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * Changes for Mime4J: + * extends org.apache.james.mime4j.field.ParseException + * added serialVersionUID + * added constructor ParseException(Throwable) + * default detail message is "Cannot parse field" + */ +public class ParseException extends Exception { + + private static final long serialVersionUID = 1L; + + + /** + * Constructs a new parse exception with the specified detail message and + * cause. + * + * @param message + * detail message + * @param cause + * the cause + */ + protected ParseException(String message, Throwable cause) { + super(message, cause); + } + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + *
+ * ParseException: <result of getMessage> + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super("Cannot parse field"); + specialConstructor = false; + } + + public ParseException(Throwable cause) { + super(cause); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/QuotedPrintableInputStream.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/QuotedPrintableInputStream.java new file mode 100644 index 0000000..7c570f9 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/QuotedPrintableInputStream.java @@ -0,0 +1,317 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Performs Quoted-Printable decoding on an underlying stream. + */ +public class QuotedPrintableInputStream extends InputStream { + + private static final int DEFAULT_BUFFER_SIZE = 1024 * 2; + + private static final byte EQ = 0x3D; + private static final byte CR = 0x0D; + private static final byte LF = 0x0A; + + private final byte[] singleByte = new byte[1]; + + private final InputStream in; + private final ByteArrayBuffer decodedBuf; + private final ByteArrayBuffer blanks; + + private final byte[] encoded; + private int pos = 0; // current index into encoded buffer + private int limit = 0; // current size of encoded buffer + + private boolean lastWasCR = false; + private boolean closed; + + private final DecodeMonitor monitor; + + public QuotedPrintableInputStream(final InputStream in, DecodeMonitor monitor) { + this(DEFAULT_BUFFER_SIZE, in, monitor); + } + + protected QuotedPrintableInputStream(final int bufsize, final InputStream in, DecodeMonitor monitor) { + super(); + this.in = in; + this.encoded = new byte[bufsize]; + this.decodedBuf = new ByteArrayBuffer(512); + this.blanks = new ByteArrayBuffer(512); + this.closed = false; + this.monitor = monitor; + } + + protected QuotedPrintableInputStream(final int bufsize, final InputStream in, boolean strict) { + this(bufsize, in, strict ? DecodeMonitor.STRICT : DecodeMonitor.SILENT); + } + + public QuotedPrintableInputStream(final InputStream in, boolean strict) { + this(DEFAULT_BUFFER_SIZE, in, strict); + } + + public QuotedPrintableInputStream(final InputStream in) { + this(in, false); + } + + /** + * Terminates Quoted-Printable coded content. This method does NOT close + * the underlying input stream. + * + * @throws IOException on I/O errors. + */ + @Override + public void close() throws IOException { + closed = true; + } + + private int fillBuffer() throws IOException { + // Compact buffer if needed + if (pos < limit) { + System.arraycopy(encoded, pos, encoded, 0, limit - pos); + limit -= pos; + pos = 0; + } else { + limit = 0; + pos = 0; + } + + int capacity = encoded.length - limit; + if (capacity > 0) { + int bytesRead = in.read(encoded, limit, capacity); + if (bytesRead > 0) { + limit += bytesRead; + } + return bytesRead; + } else { + return 0; + } + } + + private int getnext() { + if (pos < limit) { + byte b = encoded[pos]; + pos++; + return b & 0xFF; + } else { + return -1; + } + } + + private int peek(int i) { + if (pos + i < limit) { + return encoded[pos + i] & 0xFF; + } else { + return -1; + } + } + + private int transfer( + final int b, final byte[] buffer, final int from, final int to, boolean keepblanks) throws IOException { + int index = from; + if (keepblanks && blanks.length() > 0) { + int chunk = Math.min(blanks.length(), to - index); + System.arraycopy(blanks.buffer(), 0, buffer, index, chunk); + index += chunk; + int remaining = blanks.length() - chunk; + if (remaining > 0) { + decodedBuf.append(blanks.buffer(), chunk, remaining); + } + blanks.clear(); + } else if (blanks.length() > 0 && !keepblanks) { + StringBuilder sb = new StringBuilder(blanks.length() * 3); + for (int i = 0; i < blanks.length(); i++) sb.append(" ").append(blanks.byteAt(i)); + if (monitor.warn("ignored blanks", sb.toString())) + throw new IOException("ignored blanks"); + } + if (b != -1) { + if (index < to) { + buffer[index++] = (byte) b; + } else { + decodedBuf.append(b); + } + } + return index; + } + + private int read0(final byte[] buffer, final int off, final int len) throws IOException { + boolean eof = false; + int to = off + len; + int index = off; + + // check if a previous invocation left decoded content + if (decodedBuf.length() > 0) { + int chunk = Math.min(decodedBuf.length(), to - index); + System.arraycopy(decodedBuf.buffer(), 0, buffer, index, chunk); + decodedBuf.remove(0, chunk); + index += chunk; + } + + while (index < to) { + + if (limit - pos < 3) { + int bytesRead = fillBuffer(); + eof = bytesRead == -1; + } + + // end of stream? + if (limit - pos == 0 && eof) { + return index == off ? -1 : index - off; + } + + while (pos < limit && index < to) { + int b = encoded[pos++] & 0xFF; + + if (lastWasCR && b != LF) { + if (monitor.warn("Found CR without LF", "Leaving it as is")) { + throw new IOException("Found CR without LF"); + } + index = transfer(CR, buffer, index, to, false); + } else if (!lastWasCR && b == LF) { + if (monitor.warn("Found LF without CR", "Translating to CRLF")) { + throw new IOException("Found LF without CR"); + } + } + + if (b == CR) { + lastWasCR = true; + continue; + } else { + lastWasCR = false; + } + + if (b == LF) { + // at end of line + if (blanks.length() == 0) { + index = transfer(CR, buffer, index, to, false); + index = transfer(LF, buffer, index, to, false); + } else { + if (blanks.byteAt(0) != EQ) { + // hard line break + index = transfer(CR, buffer, index, to, false); + index = transfer(LF, buffer, index, to, false); + } + } + blanks.clear(); + } else if (b == EQ) { + if (limit - pos < 2 && !eof) { + // not enough buffered data + pos--; + break; + } + + // found special char '=' + int b2 = getnext(); + if (b2 == EQ) { + index = transfer(b2, buffer, index, to, true); + // deal with '==\r\n' brokenness + int bb1 = peek(0); + int bb2 = peek(1); + if (bb1 == LF || (bb1 == CR && bb2 == LF)) { + monitor.warn("Unexpected ==EOL encountered", "== 0x"+bb1+" 0x"+bb2); + blanks.append(b2); + } else { + monitor.warn("Unexpected == encountered", "=="); + } + } else if (Character.isWhitespace((char) b2)) { + // soft line break + int b3 = peek(0); + if (!(b2 == CR && b3 == LF)) { + if (monitor.warn("Found non-standard soft line break", "Translating to soft line break")) { + throw new IOException("Non-standard soft line break"); + } + } + if (b3 == LF) { + lastWasCR = b2 == CR; + } + index = transfer(-1, buffer, index, to, true); + if (b2 != LF) { + blanks.append(b); + blanks.append(b2); + } + } else { + int b3 = getnext(); + int upper = convert(b2); + int lower = convert(b3); + if (upper < 0 || lower < 0) { + monitor.warn("Malformed encoded value encountered", "leaving "+((char) EQ)+((char) b2)+((char) b3)+" as is"); + // TODO see MIME4J-160 + index = transfer(EQ, buffer, index, to, true); + index = transfer(b2, buffer, index, to, false); + index = transfer(b3, buffer, index, to, false); + } else { + index = transfer((upper << 4) | lower, buffer, index, to, true); + } + } + } else if (Character.isWhitespace(b)) { + blanks.append(b); + } else { + index = transfer(b & 0xFF, buffer, index, to, true); + } + } + } + return to - off; + } + + /** + * Converts '0' => 0, 'A' => 10, etc. + * @param c ASCII character value. + * @return Numeric value of hexadecimal character. + */ + private int convert(int c) { + if (c >= '0' && c <= '9') { + return (c - '0'); + } else if (c >= 'A' && c <= 'F') { + return (0xA + (c - 'A')); + } else if (c >= 'a' && c <= 'f') { + return (0xA + (c - 'a')); + } else { + return -1; + } + } + + @Override + public int read() throws IOException { + if (closed) { + throw new IOException("Stream has been closed"); + } + for (;;) { + int bytes = read(singleByte, 0, 1); + if (bytes == -1) { + return -1; + } + if (bytes == 1) { + return singleByte[0] & 0xff; + } + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (closed) { + throw new IOException("Stream has been closed"); + } + return read0(b, off, len); + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/QuotedPrintableOutputStream.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/QuotedPrintableOutputStream.java new file mode 100644 index 0000000..c0147cf --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/QuotedPrintableOutputStream.java @@ -0,0 +1,239 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Performs Quoted-Printable encoding on an underlying stream. + * + * Encodes every "required" char plus the dot ".". We encode the dot + * by default because this is a workaround for some "filter"/"antivirus" + * "old mua" having issues with dots at the beginning or the end of a + * qp encode line (maybe a bad dot-destuffing algo). + */ +public class QuotedPrintableOutputStream extends FilterOutputStream { + + private static final int DEFAULT_BUFFER_SIZE = 1024 * 3; + + private static final byte TB = 0x09; + private static final byte SP = 0x20; + private static final byte EQ = 0x3D; + private static final byte DOT = 0x2E; + private static final byte CR = 0x0D; + private static final byte LF = 0x0A; + private static final byte QUOTED_PRINTABLE_LAST_PLAIN = 0x7E; + private static final int QUOTED_PRINTABLE_MAX_LINE_LENGTH = 76; + private static final int QUOTED_PRINTABLE_OCTETS_PER_ESCAPE = 3; + private static final byte[] HEX_DIGITS = { + '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; + + private final byte[] outBuffer; + private final boolean binary; + + private boolean pendingSpace; + private boolean pendingTab; + private boolean pendingCR; + private int nextSoftBreak; + private int outputIndex; + + private boolean closed = false; + + private final byte[] singleByte = new byte[1]; + + public QuotedPrintableOutputStream(int bufsize, OutputStream out, boolean binary) { + super(out); + this.outBuffer = new byte[bufsize]; + this.binary = binary; + this.pendingSpace = false; + this.pendingTab = false; + this.pendingCR = false; + this.outputIndex = 0; + this.nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH + 1; + } + + public QuotedPrintableOutputStream(OutputStream out, boolean binary) { + this(DEFAULT_BUFFER_SIZE, out, binary); + } + + private void encodeChunk(byte[] buffer, int off, int len) throws IOException { + for (int inputIndex = off; inputIndex < len + off; inputIndex++) { + encode(buffer[inputIndex]); + } + } + + private void completeEncoding() throws IOException { + writePending(); + flushOutput(); + } + + private void writePending() throws IOException { + if (pendingSpace) { + plain(SP); + } else if (pendingTab) { + plain(TB); + } else if (pendingCR) { + plain(CR); + } + clearPending(); + } + + private void clearPending() throws IOException { + pendingSpace = false; + pendingTab = false; + pendingCR = false; + } + + private void encode(byte next) throws IOException { + if (next == LF) { + if (binary) { + writePending(); + escape(next); + } else { + if (pendingCR) { + // Expect either space or tab pending + // but not both + if (pendingSpace) { + escape(SP); + } else if (pendingTab) { + escape(TB); + } + lineBreak(); + clearPending(); + } else { + writePending(); + plain(next); + } + } + } else if (next == CR) { + if (binary) { + escape(next); + } else { + pendingCR = true; + } + } else { + writePending(); + if (next == SP) { + if (binary) { + escape(next); + } else { + pendingSpace = true; + } + } else if (next == TB) { + if (binary) { + escape(next); + } else { + pendingTab = true; + } + } else if (next < SP) { + escape(next); + } else if (next > QUOTED_PRINTABLE_LAST_PLAIN) { + escape(next); + } else if (next == EQ || next == DOT) { + escape(next); + } else { + plain(next); + } + } + } + + private void plain(byte next) throws IOException { + if (--nextSoftBreak <= 1) { + softBreak(); + } + write(next); + } + + private void escape(byte next) throws IOException { + if (--nextSoftBreak <= QUOTED_PRINTABLE_OCTETS_PER_ESCAPE) { + softBreak(); + } + + int nextUnsigned = next & 0xff; + + write(EQ); + --nextSoftBreak; + write(HEX_DIGITS[nextUnsigned >> 4]); + --nextSoftBreak; + write(HEX_DIGITS[nextUnsigned % 0x10]); + } + + private void write(byte next) throws IOException { + outBuffer[outputIndex++] = next; + if (outputIndex >= outBuffer.length) { + flushOutput(); + } + } + + private void softBreak() throws IOException { + write(EQ); + lineBreak(); + } + + private void lineBreak() throws IOException { + write(CR); + write(LF); + nextSoftBreak = QUOTED_PRINTABLE_MAX_LINE_LENGTH; + } + + void flushOutput() throws IOException { + if (outputIndex < outBuffer.length) { + out.write(outBuffer, 0, outputIndex); + } else { + out.write(outBuffer); + } + outputIndex = 0; + } + + @Override + public void close() throws IOException { + if (closed) + return; + + try { + completeEncoding(); + // do not close the wrapped stream + } finally { + closed = true; + } + } + + @Override + public void flush() throws IOException { + flushOutput(); + } + + @Override + public void write(int b) throws IOException { + singleByte[0] = (byte) b; + this.write(singleByte, 0, 1); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (closed) { + throw new IOException("Stream has been closed"); + } + encodeChunk(b, off, len); + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/SimpleCharStream.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/SimpleCharStream.java new file mode 100644 index 0000000..3dc960b --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/SimpleCharStream.java @@ -0,0 +1,489 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 5.0 */ +/* JavaCCOptions:STATIC=false,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ +/** Whether parser is static. */ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; +/** Position in buffer. */ + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + +/** Start. */ + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + +/** Read a character. */ + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return c; + } + + @Deprecated + /** + * @deprecated + * @see #getEndColumn + */ + + public int getColumn() { + return bufcolumn[bufpos]; + } + + @Deprecated + /** + * @deprecated + * @see #getEndLine + */ + + public int getLine() { + return bufline[bufpos]; + } + + /** Get token end column number. */ + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + /** Get token end line number. */ + public int getEndLine() { + return bufline[bufpos]; + } + + /** Get token beginning column number. */ + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + /** Get token beginning line number. */ + public int getBeginLine() { + return bufline[tokenBegin]; + } + +/** Backup a number of characters. */ + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + /** Constructor. */ + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + /** Constructor. */ + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + /** Constructor. */ + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + /** Reinitialise. */ + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + /** Constructor. */ + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + /** Reinitialise. */ + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + /** Get token literal value. */ + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + /** Get the suffix. */ + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + /** Reset buffer when finished. */ + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} +/* JavaCC - OriginalChecksum=3a0a96dd40a434f5b242b9ed9202faa4 (do not edit this line) */ diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/TextInputStream.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/TextInputStream.java new file mode 100644 index 0000000..2940cc3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/TextInputStream.java @@ -0,0 +1,156 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +/** + * {@link InputStream} backed by {@link CharSequence}. + */ +class TextInputStream extends InputStream { + + private final CharsetEncoder encoder; + private final CharBuffer cbuf; + private final ByteBuffer bbuf; + + private int mark = -1; + + TextInputStream(final CharSequence s, final Charset charset, int bufferSize) { + super(); + this.encoder = charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + this.bbuf = ByteBuffer.allocate(bufferSize); + this.bbuf.flip(); + this.cbuf = CharBuffer.wrap(s); + } + + private void fillBuffer() throws CharacterCodingException { + this.bbuf.compact(); + CoderResult result = this.encoder.encode(this.cbuf, this.bbuf, true); + if (result.isError()) { + result.throwException(); + } + this.bbuf.flip(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } + if (off < 0 || len < 0 || off + len > b.length) { + throw new IndexOutOfBoundsException(); + } + if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) { + return -1; + } + int bytesRead = 0; + while (len > 0) { + if (this.bbuf.hasRemaining()) { + int chunk = Math.min(this.bbuf.remaining(), len); + this.bbuf.get(b, off, chunk); + off += chunk; + len -= chunk; + bytesRead += chunk; + } else { + fillBuffer(); + if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) { + break; + } + } + } + if (bytesRead > 0) { + return bytesRead; + } else { + if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) { + return -1; + } else { + return bytesRead; + } + } + } + + @Override + public int read() throws IOException { + for (;;) { + if (this.bbuf.hasRemaining()) { + return this.bbuf.get() & 0xFF; + } else { + fillBuffer(); + if (!this.bbuf.hasRemaining() && !this.cbuf.hasRemaining()) { + return -1; + } + } + } + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public long skip(long n) throws IOException { + int skipped = 0; + while (n > 0 && this.cbuf.hasRemaining()) { + this.cbuf.get(); + n--; + skipped++; + } + return skipped; + } + + @Override + public int available() throws IOException { + return this.cbuf.remaining(); + } + + @Override + public void mark(int readlimit) { + this.mark = this.cbuf.position(); + } + + @Override + public void reset() throws IOException { + if (this.mark != -1) { + this.cbuf.position(this.mark); + this.mark = -1; + } + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void close() throws IOException { + } + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Token.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Token.java new file mode 100644 index 0000000..5c231b3 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/Token.java @@ -0,0 +1,149 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 5.0 */ +/* JavaCCOptions:TOKEN_EXTENDS=,KEEP_LINE_COL=null,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +/** + * Describes the input token stream. + */ + +public class Token implements java.io.Serializable { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** The line number of the first character of this Token. */ + public int beginLine; + /** The column number of the first character of this Token. */ + public int beginColumn; + /** The line number of the last character of this Token. */ + public int endLine; + /** The column number of the last character of this Token. */ + public int endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * An optional attribute value of the Token. + * Tokens which are not used as syntactic sugar will often contain + * meaningful values that will be used later on by the compiler or + * interpreter. This attribute value is often different from the image. + * Any subclass of Token that actually wants to return a non-null value can + * override this method as appropriate. + */ + public Object getValue() { + return null; + } + + /** + * No-argument constructor + */ + public Token() {} + + /** + * Constructs a new token for the specified Image. + */ + public Token(int kind) + { + this(kind, null); + } + + /** + * Constructs a new token for the specified Image and Kind. + */ + public Token(int kind, String image) + { + this.kind = kind; + this.image = image; + } + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simply add something like : + * + * case MyParserConstants.ID : return new IDToken(ofKind, image); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use sit in your lexical actions. + */ + public static Token newToken(int ofKind, String image) + { + switch(ofKind) + { + default : return new Token(ofKind, image); + } + } + + public static Token newToken(int ofKind) + { + return newToken(ofKind, null); + } + +} +/* JavaCC - OriginalChecksum=b1ee9aaf7cbe47fcd350a0df848362b5 (do not edit this line) */ diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/TokenMgrError.java b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/TokenMgrError.java new file mode 100644 index 0000000..548f237 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/contentdisposition/TokenMgrError.java @@ -0,0 +1,165 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 5.0 */ +/* JavaCCOptions: */ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 * + * * + * http://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.mozilla.xiu.browser.utils.contentdisposition; + +/** Token Manager Error. */ +public class TokenMgrError extends Error +{ + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occurred. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt was made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their escaped (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexical error + * curLexState : lexical state in which this error occurred + * errorLine : line number when the error occurred + * errorColumn : column number when the error occurred + * errorAfter : prefix that was seen before this error occurred + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + /** No arg constructor. */ + public TokenMgrError() { + } + + /** Constructor with message and reason. */ + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + /** Full Constructor. */ + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} +/* JavaCC - OriginalChecksum=9fa2b884a567f978be9291126c2a21c0 (do not edit this line) */ diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/FilePicker.kt b/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/FilePicker.kt new file mode 100644 index 0000000..5ac6f0e --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/FilePicker.kt @@ -0,0 +1,83 @@ +package org.mozilla.xiu.browser.utils.filePicker + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity + +class FilePicker { + private var uriListener: UriListener? = null + private lateinit var getContent: ActivityResultLauncher + var activity: FragmentActivity + + constructor(getContent: ActivityResultLauncher,activity: FragmentActivity) { + this.getContent=getContent + this.activity=activity + + } + + fun open(activity: FragmentActivity, mimeTypes: Array ) { + getContent.launch(true) + requestPermission() + + } + + fun putUriListener(uriListener: UriListener?) { + this.uriListener = uriListener + } + + fun putUri(uri: Uri?) { + uriListener!!.UriGet(uri) + } + + // 状态变化监听 + interface UriListener { + // 回调方法 + fun UriGet(uri: Uri?) + } + + private fun requestPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // 先判断有没有权限 + if (Environment.isExternalStorageManager()) { + } else { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + intent.data = Uri.parse("package:" + activity.getPackageName()) + activity.startActivityForResult(intent, 1024) + Toast.makeText(activity, "请先授予权限", Toast.LENGTH_SHORT).show() + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // 先判断有没有权限 + if (ActivityCompat.checkSelfPermission( + activity, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + activity, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + ) { + } else { + ActivityCompat.requestPermissions( + activity, + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ), + 1024 + ) + } + } + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/GetFile.java b/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/GetFile.java new file mode 100644 index 0000000..dac1e0a --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/GetFile.java @@ -0,0 +1,60 @@ +package org.mozilla.xiu.browser.utils.filePicker; + +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import androidx.fragment.app.FragmentActivity; + +import org.jetbrains.annotations.Nullable; + + +public class GetFile { + Uri uri; + Handler mHandler ; + FilePicker filePicker; + public GetFile(FragmentActivity activity,FilePicker filePicker){ + this.filePicker=filePicker; + + filePicker.putUriListener(new FilePicker.UriListener() { + @Override + public void UriGet(Uri uri) { + close(uri); + } + }); + } + public void open(FragmentActivity activity, @Nullable String[] mimeTypes){ + mHandler = new Handler() { + @Override + public void handleMessage(Message mesg) { + // process incoming messages here + //super.handleMessage(msg); + throw new RuntimeException(); + } + }; + filePicker.open(activity,mimeTypes); + try { + Looper.getMainLooper().loop(); + } + catch(RuntimeException e2) + { + } + + } + public void close(Uri uri){ + setUri(uri); + Message m = mHandler.obtainMessage(); + mHandler.sendMessage(m); + } + + public Uri getUri() { + return uri; + } + + public void setUri(Uri uri) { + this.uri = uri; + } + + +} diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/PickUtils.java b/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/PickUtils.java new file mode 100644 index 0000000..f65a453 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/PickUtils.java @@ -0,0 +1,276 @@ +package org.mozilla.xiu.browser.utils.filePicker; + + +import android.annotation.SuppressLint; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + + +public class PickUtils { + public static final String DOCUMENTS_DIR = "documents"; + + @SuppressLint("NewApi") + public static String getPath(final Context context, final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + + if (id != null && id.startsWith("raw:")) { + return id.substring(4); + } + + String[] contentUriPrefixesToTry = new String[]{ + "content://downloads/public_downloads", + "content://downloads/my_downloads" + }; + for (String contentUriPrefix : contentUriPrefixesToTry) { + try { + // note: id 可能为字符串,如在华为10.0系统上,选择文件后id为:"msf:254",导致转Long异常 + Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.parseLong(id)); + String path = getDataColumn(context, contentUri, null, null); + if (path != null && !path.equals("")) { + return path; + } + } catch (Exception e) { + } + } + + // path could not be retrieved using ContentResolver, therefore copy file to accessible cache using streams + String fileName = getFileName(context, uri); + File cacheDir = getDocumentCacheDir(context); + File file = generateFileName(fileName, cacheDir); + String destinationPath = null; + if (file != null) { + destinationPath = file.getAbsolutePath(); + saveFileFromUri(context, uri, destinationPath); + } + return destinationPath; + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{split[1]}; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + String path = getDataColumn(context, uri, null, null); + if (path != null && !path.equals("")) return path; + + // path could not be retrieved using ContentResolver, therefore copy file to accessible cache using streams + String fileName = getFileName(context, uri); + File cacheDir = getDocumentCacheDir(context); + File file = generateFileName(fileName, cacheDir); + String destinationPath = null; + if (file != null) { + destinationPath = file.getAbsolutePath(); + saveFileFromUri(context, uri, destinationPath); + } + return destinationPath; + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + return null; + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String column = "_data"; + final String[] projection = {column}; + String path = ""; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + path = cursor.getString(column_index); + return path; + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (cursor != null) + cursor.close(); + } + return path; + } + + public static String getFileName(@NonNull Context context, Uri uri) { + String mimeType = context.getContentResolver().getType(uri); + String filename = null; + if (mimeType == null && context != null) { + String path = getPath(context, uri); + if (path == null) { + filename = getName(uri.toString()); + } else { + File file = new File(path); + filename = file.getName(); + } + } else { + Cursor returnCursor = context.getContentResolver().query(uri, null, + null, null, null); + if (returnCursor != null) { + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + returnCursor.moveToFirst(); + filename = returnCursor.getString(nameIndex); + returnCursor.close(); + } + } + String[] filename_s = filename.split("\\.",2); + + return filename_s[0]; + } + + public static String getName(String filename) { + if (filename == null) { + return null; + } + int index = filename.lastIndexOf('/'); + return filename.substring(index + 1); + } + + public static File getDocumentCacheDir(@NonNull Context context) { + Log.d("PickUtils", "getDocumentCacheDir"); + File dir = new File(context.getCacheDir(), DOCUMENTS_DIR); + if (!dir.exists()) { + dir.mkdirs(); + } + + return dir; + } + + @Nullable + public static File generateFileName(@Nullable String name, File directory) { + if (name == null) { + return null; + } + + File file = new File(directory, name); + + if (file.exists()) { + String fileName = name; + String extension = ""; + int dotIndex = name.lastIndexOf('.'); + if (dotIndex > 0) { + fileName = name.substring(0, dotIndex); + extension = name.substring(dotIndex); + } + + int index = 0; + + while (file.exists()) { + index++; + name = fileName + '(' + index + ')' + extension; + file = new File(directory, name); + } + } + try { + if (!file.createNewFile()) { + return null; + } + } catch (IOException e) { + return null; + } + return file; + } + + private static void saveFileFromUri(Context context, Uri uri, String destinationPath) { + InputStream is = null; + BufferedOutputStream bos = null; + try { + is = context.getContentResolver().openInputStream(uri); + bos = new BufferedOutputStream(new FileOutputStream(destinationPath, false)); + byte[] buf = new byte[1024]; + is.read(buf); + do { + bos.write(buf); + } while (is.read(buf) != -1); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (is != null) is.close(); + if (bos != null) bos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} + diff --git a/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/ResultContract.java b/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/ResultContract.java new file mode 100644 index 0000000..c589fe9 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/utils/filePicker/ResultContract.java @@ -0,0 +1,26 @@ +package org.mozilla.xiu.browser.utils.filePicker; + +import android.content.Context; +import android.content.Intent; + +import androidx.activity.result.contract.ActivityResultContract; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ResultContract extends ActivityResultContract { + @NonNull + @Override + public Intent createIntent(@NonNull Context context, Boolean input) { + Intent intent = new Intent(); + intent.setType("*/*");//同时选择视频和图片 + intent.setAction(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + // Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + return intent; + } + @Override + public Intent parseResult(int resultCode, @Nullable Intent intent) { + return intent; + } +} + diff --git a/app/src/main/java/org/mozilla/xiu/browser/view/MyWebProgress.java b/app/src/main/java/org/mozilla/xiu/browser/view/MyWebProgress.java new file mode 100644 index 0000000..4398152 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/view/MyWebProgress.java @@ -0,0 +1,451 @@ +package org.mozilla.xiu.browser.view; + +/* + * Copyright 2019. Bin Jing (https://github.com/youlookwhat) + * + * 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 + * + * http://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. + */ + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; + +import androidx.annotation.Nullable; + +import com.google.android.material.color.MaterialColors; + +/** + * WebView进度条,原作者: cenxiaozhong,在此基础上修改优化: + * 1. progress同时返回两次100时进度条出现两次 + * 2. 当一条进度没跑完,又点击其他链接开始第二次进度时,第二次进度不出现 + * 3. 修改消失动画时长,使其消失时看到可以进度跑完 + * 4. [2019.9.29] 修复当第一次进度返回 0 或超过 10,出现不显示进度条的问题 + * 5. 能显示渐变色 + * 6. 进度在95-100时再次开始进度条透明度问题 + * + * @author jingbin + * Link to https://github.com/youlookwhat/WebProgress + */ +public class MyWebProgress extends FrameLayout { + + /** + * 默认匀速动画最大的时长 + */ + public static final int MAX_UNIFORM_SPEED_DURATION = 8 * 1000; + /** + * 默认加速后减速动画最大时长 + */ + public static final int MAX_DECELERATE_SPEED_DURATION = 350; + /** + * 95f-100f时,透明度1f-0f时长 + */ + public static final int DO_END_ALPHA_DURATION = 330; + /** + * 95f - 100f动画时长 + */ + public static final int DO_END_PROGRESS_DURATION = 200; + /** + * 当前匀速动画最大的时长 + */ + private static int CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION; + /** + * 当前加速后减速动画最大时长 + */ + private static int CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION; + /** + * 默认的高度(dp) + */ + public static int WEB_PROGRESS_DEFAULT_HEIGHT = 3; + /** + * 进度条颜色默认 + */ + public static String WEB_PROGRESS_COLOR = "#62A6FB"; + /** + * 进度条颜色 + */ + private int mColor; + /** + * 进度条的画笔 + */ + private Paint mPaint; + /** + * 进度条动画 + */ + private Animator mAnimator; + /** + * 控件的宽度 + */ + private int mTargetWidth = 0; + /** + * 控件的高度 + */ + private int mTargetHeight; + /** + * 标志当前进度条的状态 + */ + private int TAG = 0; + /** + * 第一次过来进度show,后面就是setProgress + */ + private boolean isShow = false; + public static final int UN_START = 0; + public static final int STARTED = 1; + public static final int FINISH = 2; + private float mCurrentProgress = 0F; + private long ticket = 0L; + + public MyWebProgress(Context context) { + this(context, null); + } + + public MyWebProgress(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public MyWebProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + mPaint = new Paint(); + mColor = MaterialColors.getColor(getContext(), com.google.android.material.R.attr.colorPrimary, Color.parseColor(WEB_PROGRESS_COLOR)); + Log.d("test", "init onProgressChange"); + mPaint.setAntiAlias(true); + mPaint.setColor(mColor); + mPaint.setDither(true); + mPaint.setStrokeCap(Paint.Cap.SQUARE); + + mTargetWidth = context.getResources().getDisplayMetrics().widthPixels; + mTargetHeight = dip2px(WEB_PROGRESS_DEFAULT_HEIGHT); + } + + /** + * 设置单色进度条 + */ + public void setColor(int color) { + this.mColor = color; + mPaint.setColor(color); + } + + public void setColor(String color) { + this.setColor(Color.parseColor(color)); + } + + public void setColor(int startColor, int endColor) { + LinearGradient linearGradient = new LinearGradient(0, 0, mTargetWidth, mTargetHeight, startColor, endColor, Shader.TileMode.CLAMP); + mPaint.setShader(linearGradient); + } + + /** + * 设置渐变色进度条 + * + * @param startColor 开始颜色 + * @param endColor 结束颜色 + */ + public void setColor(String startColor, String endColor) { + this.setColor(Color.parseColor(startColor), Color.parseColor(endColor)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int wMode = MeasureSpec.getMode(widthMeasureSpec); + int w = MeasureSpec.getSize(widthMeasureSpec); + + int hMode = MeasureSpec.getMode(heightMeasureSpec); + int h = MeasureSpec.getSize(heightMeasureSpec); + + if (wMode == MeasureSpec.AT_MOST) { + w = Math.min(w, getContext().getResources().getDisplayMetrics().widthPixels); + } + if (hMode == MeasureSpec.AT_MOST) { + h = mTargetHeight; + } + this.setMeasuredDimension(w, h); + } + + @Override + protected void onDraw(Canvas canvas) { + + } + + @Override + protected void dispatchDraw(Canvas canvas) { + canvas.drawRect(0, 0, mCurrentProgress / 100 * (float) this.getWidth(), this.getHeight(), mPaint); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + this.mTargetWidth = getMeasuredWidth(); + int screenWidth = getContext().getResources().getDisplayMetrics().widthPixels; + if (mTargetWidth >= screenWidth) { + CURRENT_MAX_DECELERATE_SPEED_DURATION = MAX_DECELERATE_SPEED_DURATION; + CURRENT_MAX_UNIFORM_SPEED_DURATION = MAX_UNIFORM_SPEED_DURATION; + } else { + //取比值 + float rate = this.mTargetWidth / (float) screenWidth; + CURRENT_MAX_UNIFORM_SPEED_DURATION = (int) (MAX_UNIFORM_SPEED_DURATION * rate); + CURRENT_MAX_DECELERATE_SPEED_DURATION = (int) (MAX_DECELERATE_SPEED_DURATION * rate); + } + } + + private void setFinish() { + isShow = false; + TAG = FINISH; + } + + private void startAnim(boolean isFinished) { + + float v = isFinished ? 100 : 95; + + if (mAnimator != null && mAnimator.isStarted()) { + mAnimator.cancel(); + } + mCurrentProgress = mCurrentProgress == 0 ? 0.00000001f : mCurrentProgress; + // 可能由于透明度造成突然出现的问题 + if (isFinished && getAlpha() < 0.5f) { + //本来就要让它消失 + } else { + setAlpha(1); + } + + ticket = System.currentTimeMillis(); + if (!isFinished) { + if (mCurrentProgress < 30) { + ValueAnimator mAnimator1 = ValueAnimator.ofFloat(mCurrentProgress, 30f); + float residue = 1f - mCurrentProgress / 100 - 0.05f; + long duration = (long) (residue * CURRENT_MAX_UNIFORM_SPEED_DURATION); + long duration2 = Math.min((long) (duration * 0.3f), 200L); + long duration3 = duration - duration2; + mAnimator1.setInterpolator(new AccelerateInterpolator()); + mAnimator1.setDuration(duration2); + mAnimator1.addUpdateListener(mAnimatorUpdateListener); + ValueAnimator mAnimator2 = ValueAnimator.ofFloat(30f, v); + mAnimator2.setInterpolator(new LinearInterpolator()); + mAnimator2.setDuration(duration3); + mAnimator2.addUpdateListener(mAnimatorUpdateListener); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playSequentially(mAnimator1, mAnimator2); + animatorSet.start(); + this.mAnimator = animatorSet; + } else { + ValueAnimator mAnimator = ValueAnimator.ofFloat(mCurrentProgress, v); + float residue = 1f - mCurrentProgress / 100 - 0.05f; + mAnimator.setInterpolator(new LinearInterpolator()); + mAnimator.setDuration((long) (residue * CURRENT_MAX_UNIFORM_SPEED_DURATION)); + mAnimator.addUpdateListener(mAnimatorUpdateListener); + mAnimator.start(); + this.mAnimator = mAnimator; + } + } else { + + ValueAnimator segment95Animator = null; + if (mCurrentProgress < 95) { + segment95Animator = ValueAnimator.ofFloat(mCurrentProgress, 95); + float residue = 1f - mCurrentProgress / 100f - 0.05f; + segment95Animator.setInterpolator(new LinearInterpolator()); + segment95Animator.setDuration((long) (residue * CURRENT_MAX_DECELERATE_SPEED_DURATION)); + segment95Animator.setInterpolator(new DecelerateInterpolator()); + segment95Animator.addUpdateListener(mAnimatorUpdateListener); + } + + ObjectAnimator mObjectAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f); + mObjectAnimator.setDuration(DO_END_ALPHA_DURATION); + ValueAnimator mValueAnimatorEnd = ValueAnimator.ofFloat(95f, 100f); + mValueAnimatorEnd.setDuration(DO_END_PROGRESS_DURATION); + mValueAnimatorEnd.addUpdateListener(mAnimatorUpdateListener); + + AnimatorSet mAnimatorSet = new AnimatorSet(); + mAnimatorSet.playTogether(mObjectAnimator, mValueAnimatorEnd); + + if (segment95Animator != null) { + AnimatorSet mAnimatorSet1 = new AnimatorSet(); + mAnimatorSet1.play(mAnimatorSet).after(segment95Animator); + mAnimatorSet = mAnimatorSet1; + } + mAnimatorSet.addListener(new MyAnimatorListenerAdapter(ticket) { + @Override + public void onAnimationEnd(Animator animation) { + if (bindTicket == ticket) { + doEnd(); + } + } + }); + mAnimatorSet.start(); + mAnimator = mAnimatorSet; + } + + TAG = STARTED; + } + + private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = (float) animation.getAnimatedValue(); + mCurrentProgress = t; + invalidate(); + } + }; + + private static abstract class MyAnimatorListenerAdapter extends AnimatorListenerAdapter { + long bindTicket = 0L; + + MyAnimatorListenerAdapter(long bindTicket) { + this.bindTicket = bindTicket; + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + /** + * animator cause leak , if not cancel; + */ + if (mAnimator != null && mAnimator.isStarted()) { + mAnimator.cancel(); + mAnimator = null; + } + } + + private void doEnd() { + setVisibility(GONE); + mCurrentProgress = 0f; + this.setAlpha(1f); + TAG = UN_START; + } + + public void reset() { + mCurrentProgress = 0; + if (mAnimator != null && mAnimator.isStarted()) { + mAnimator.cancel(); + } + } + + public void setProgress(int newProgress) { + setProgress(Float.valueOf(newProgress)); + } + + + public LayoutParams offerLayoutParams() { + return new LayoutParams(mTargetWidth, mTargetHeight); + } + + private int dip2px(float dpValue) { + final float scale = getContext().getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + public MyWebProgress setHeight(int heightDp) { + this.mTargetHeight = dip2px(heightDp); + return this; + } + + public void setProgress(float progress) { + //Timber.d("setProgress: %s, TAG: %s", progress, TAG); + // fix 同时返回两个 100,产生两次进度条的问题; + if (TAG == UN_START && progress == 100) { + setVisibility(View.GONE); + return; + } + if (TAG == FINISH && progress == 100) { + //正在消失中 + return; + } + + if (getVisibility() == View.GONE) { + setVisibility(View.VISIBLE); + } + if (progress < 95) { + if (progress > 30 && progress - mCurrentProgress > 3) { + float v = 95; + if (mAnimator != null && mAnimator.isStarted()) { + mAnimator.cancel(); + } + mCurrentProgress = mCurrentProgress == 0 ? 0.00000001f : mCurrentProgress; + // 可能由于透明度造成突然出现的问题 + setAlpha(1); + ValueAnimator mAnimator1 = ValueAnimator.ofFloat(mCurrentProgress, progress); + float residue = 1f - mCurrentProgress / 100 - 0.05f; + long duration = (long) (residue * CURRENT_MAX_UNIFORM_SPEED_DURATION); + long duration2 = Math.min((long) (duration * progress / 100f), 200L); + long duration3 = duration - duration2; + mAnimator1.setInterpolator(new AccelerateInterpolator()); + mAnimator1.setDuration(duration2); + mAnimator1.addUpdateListener(mAnimatorUpdateListener); + ValueAnimator mAnimator2 = ValueAnimator.ofFloat(progress, v); + mAnimator2.setInterpolator(new LinearInterpolator()); + mAnimator2.setDuration(duration3); + mAnimator2.addUpdateListener(mAnimatorUpdateListener); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playSequentially(mAnimator1, mAnimator2); + animatorSet.start(); + this.mAnimator = animatorSet; + } + return; + } + if (TAG != FINISH) { + startAnim(true); + } + } + + /** + * 显示进度条 + */ + public void show() { + isShow = true; + setVisibility(View.VISIBLE); + mCurrentProgress = 0f; + startAnim(false); + } + + /** + * 进度完成后消失 + */ + public void hide() { + setWebProgress(100); + } + + /** + * 为单独处理WebView进度条 + */ + public void setWebProgress(int newProgress) { + if (newProgress >= 0 && newProgress < 95) { + if (!isShow) { + show(); + } else { + setProgress(newProgress); + } + } else { + setProgress(newProgress); + setFinish(); + } + } +} + diff --git a/app/src/main/java/org/mozilla/xiu/browser/webextension/BrowseEvent.java b/app/src/main/java/org/mozilla/xiu/browser/webextension/BrowseEvent.java new file mode 100644 index 0000000..1f0ee23 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/webextension/BrowseEvent.java @@ -0,0 +1,24 @@ +package org.mozilla.xiu.browser.webextension; + +/** + * 作者:By 15968 + * 日期:On 2023/11/8 + * 时间:At 21:50 + */ + +public class BrowseEvent { + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + private String url; + + public BrowseEvent(String url) { + this.url = url; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/webextension/WebExtensionsAddEvent.java b/app/src/main/java/org/mozilla/xiu/browser/webextension/WebExtensionsAddEvent.java new file mode 100644 index 0000000..6d43a48 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/webextension/WebExtensionsAddEvent.java @@ -0,0 +1,23 @@ +package org.mozilla.xiu.browser.webextension; + +/** + * 作者:By 15968 + * 日期:On 2023/11/8 + * 时间:At 21:50 + */ + +public class WebExtensionsAddEvent { + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + private String path; + + public WebExtensionsAddEvent(String path) { + this.path = path; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/webextension/WebExtensionsRefreshEvent.java b/app/src/main/java/org/mozilla/xiu/browser/webextension/WebExtensionsRefreshEvent.java new file mode 100644 index 0000000..6f5f4c6 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/webextension/WebExtensionsRefreshEvent.java @@ -0,0 +1,10 @@ +package org.mozilla.xiu.browser.webextension; + +/** + * 作者:By 15968 + * 日期:On 2023/11/8 + * 时间:At 21:50 + */ + +public class WebExtensionsRefreshEvent { +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/webextension/WebextensionListLiveData.kt b/app/src/main/java/org/mozilla/xiu/browser/webextension/WebextensionListLiveData.kt new file mode 100644 index 0000000..be75483 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/webextension/WebextensionListLiveData.kt @@ -0,0 +1,24 @@ +package org.mozilla.xiu.browser.webextension + +import androidx.lifecycle.LiveData +import org.mozilla.geckoview.WebExtension + +class WebextensionListLiveData: LiveData>() { + fun Value(webExtension:ArrayList){ + postValue(webExtension) + } + override fun onActive() { + super.onActive() + } + + override fun onInactive() { + super.onInactive() + } + companion object { + private lateinit var globalData: WebextensionListLiveData + fun getInstance(): WebextensionListLiveData { + globalData = if (Companion::globalData.isInitialized) globalData else WebextensionListLiveData() + return globalData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/xiu/browser/webextension/WebextensionSession.kt b/app/src/main/java/org/mozilla/xiu/browser/webextension/WebextensionSession.kt new file mode 100644 index 0000000..7a602a6 --- /dev/null +++ b/app/src/main/java/org/mozilla/xiu/browser/webextension/WebextensionSession.kt @@ -0,0 +1,199 @@ +package org.mozilla.xiu.browser.webextension + +import android.app.Activity +import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import com.alibaba.fastjson.JSON +import com.alibaba.fastjson.JSONArray +import com.alibaba.fastjson.JSONObject +import com.alibaba.fastjson.serializer.SerializerFeature +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.greenrobot.eventbus.EventBus +import org.mozilla.geckoview.AllowOrDeny +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.WebExtension +import org.mozilla.geckoview.WebExtension.InstallException +import org.mozilla.geckoview.WebExtensionController +import org.mozilla.xiu.browser.componets.HomeLivedata +import org.mozilla.xiu.browser.session.GeckoViewModel +import org.mozilla.xiu.browser.utils.FileUtil +import org.mozilla.xiu.browser.utils.ThreadTool.async +import org.mozilla.xiu.browser.utils.ThreadTool.runOnUI +import org.mozilla.xiu.browser.utils.ToastMgr +import org.mozilla.xiu.browser.utils.UriUtilsPro +import org.mozilla.xiu.browser.utils.ZipUtils +import java.io.File +import java.util.UUID + + +class WebextensionSession { + var context: Activity + private var webExtensionController: WebExtensionController + private var webExtensions = ArrayList() + + constructor(context: Activity) { + this.context = context + webExtensionController = GeckoRuntime.getDefault(context).webExtensionController + webExtensionController.list().accept { + if (it != null) { + for (i in it) + extensionDelegate(i) + } + } + } + + private fun installCrx(uri: String) { + MaterialAlertDialogBuilder(context) + .setTitle("温馨提示") + .setMessage("即将安装crx扩展程序,注意因为本软件使用火狐Gecko内核,不能完全兼容谷歌crx格式,建议能安装原生xpi扩展的情况下优先安装xpi扩展,本软件只对crx提供有限的兼容,确定继续安装crx扩展吗?") + .setPositiveButton("确定") { d, _ -> + d.dismiss() + async(Runnable { + try { + val name: String = + File(uri.replace("file://", "")).getName().replace(".crx", "") + val fileDirPath: String = + ((UriUtilsPro.getRootDir(context) + File.separator) + "_cache" + File.separator) + name + FileUtil.deleteDirs(fileDirPath) + File(fileDirPath).mkdirs() + ZipUtils.unzipFile(uri.replace("file://", ""), fileDirPath) + val m = File(fileDirPath + File.separator + "manifest.json") + if (m.exists()) { + val json: JSONObject = + JSON.parseObject(FileUtil.fileToString(m.getAbsolutePath())) + if (json.containsKey("options_page")) { + val optionsUi = JSONObject() + optionsUi.put("page", json.getString("options_page")) + optionsUi.put("open_in_tab", true) + //optionsUi.put("browser_style", false); + json.put("options_ui", optionsUi) + json.remove("options_page") + } + if (json.containsKey("permissions")) { + val permissions: JSONArray = json.getJSONArray("permissions") + if (permissions.contains("webRequest") && !permissions.contains("webRequestBlocking")) { + permissions.add( + permissions.indexOf("webRequest") + 1, + "webRequestBlocking" + ) + } + } + if (json.containsKey("background")) { + val background: JSONObject = json.getJSONObject("background") + if (background.containsKey("service_worker")) { + val scripts = JSONArray() + scripts.add(background.getString("service_worker")) + background.put("scripts", scripts) + background.remove("service_worker") + } + } + json.remove("update_url") + val gecko = JSONObject() + gecko.put("id", "{" + UUID.randomUUID().toString() + "}") + gecko.put("strict_min_version", "63.0") + val settings = JSONObject() + settings.put("gecko", gecko) + json.put("browser_specific_settings", settings) + json.remove("incognito") + json.remove("minimum_chrome_version") + FileUtil.stringToFile( + JSON.toJSONString(json, SerializerFeature.PrettyFormat), + m.getAbsolutePath() + ) + } + File(fileDirPath + File.separator + "META-INF").mkdirs() + val xpi: String = uri.replace("file://", "").replace(".crx", ".xpi") + FileUtil.deleteFile(xpi) + ZipUtils.zipFile(fileDirPath, xpi) + if (!context.isFinishing) { + runOnUI { install("file://$xpi") } + } + } catch (e: Exception) { + e.printStackTrace() + ToastMgr.shortBottomCenter(context, "出错:" + e.message) + } + }) + }.setNegativeButton("取消") { d, _ -> + d.dismiss() + }.show() + } + + fun install(uri: String) { + if (uri.startsWith("file://") && uri.endsWith(".crx")) { + installCrx(uri) + return + } + webExtensionController.promptDelegate = object : WebExtensionController.PromptDelegate { + override fun onInstallPrompt(extension: WebExtension): GeckoResult? { + val dlg = org.mozilla.xiu.browser.componets.PermissionDialog(context, extension) + if (dlg.showDialog() === 1) { + extensionDelegate(extension) + return GeckoResult.allow() + } else return GeckoResult.deny() + } + } + webExtensionController.install(uri).accept({ it -> + if (it != null) { + Toast.makeText(context, it.metaData.name + "安装成功", Toast.LENGTH_LONG).show() + EventBus.getDefault().post(WebExtensionsRefreshEvent()) + } else { + Toast.makeText(context, "安装失败", Toast.LENGTH_LONG).show() + } + }, { exception: Throwable? -> + exception?.printStackTrace() + if (exception is InstallException) { + if (exception.code == InstallException.ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED) { + MaterialAlertDialogBuilder(context) + .setTitle("安装失败") + .setMessage( + "检测到您安装的扩展程序没有正确的签名,点击下方确定按钮打开about:config页面," + + "请搜索sign滑动到最后将xpinstall.signatures.required切换为false,然后再重新安装" + ) + .setPositiveButton("确定") { d, _ -> + d.dismiss() + EventBus.getDefault().post(BrowseEvent("about:config")) + }.setNegativeButton("取消") { d, _ -> + d.dismiss() + }.show() + return@accept + } else if (exception.code == InstallException.ErrorCodes.ERROR_CORRUPT_FILE) { + MaterialAlertDialogBuilder(context) + .setTitle("安装失败") + .setMessage("文件无法识别,请确认是xpi或crx扩展程序安装包文件") + .setPositiveButton("确定") { d, _ -> + d.dismiss() + EventBus.getDefault().post(BrowseEvent("about:config")) + }.setNegativeButton("取消") { d, _ -> + d.dismiss() + }.show() + return@accept + } + } + Toast.makeText(context, "安装失败: $exception", Toast.LENGTH_LONG).show() + }) + } + + fun extensionDelegate(extension: WebExtension) { + extension.tabDelegate = object : WebExtension.TabDelegate { + override fun onNewTab( + source: WebExtension, + createDetails: WebExtension.CreateTabDetails + ): GeckoResult? { + val session = GeckoSession() + //Log.d("onNewTab",session.) + newSession(session, context) + return GeckoResult.fromValue(session) + } + } + } + + fun newSession(session: GeckoSession, activity: Activity) { + val geckoViewModel = + activity?.let { ViewModelProvider(it as ViewModelStoreOwner)[GeckoViewModel::class.java] }!! + geckoViewModel.changeSearch(session) + HomeLivedata.getInstance().Value(false) + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/alpha_enter2.xml b/app/src/main/res/anim/alpha_enter2.xml new file mode 100644 index 0000000..a14b67d --- /dev/null +++ b/app/src/main/res/anim/alpha_enter2.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/alpha_exit.xml b/app/src/main/res/anim/alpha_exit.xml new file mode 100644 index 0000000..a863005 --- /dev/null +++ b/app/src/main/res/anim/alpha_exit.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/anim/alpha_exit2.xml b/app/src/main/res/anim/alpha_exit2.xml new file mode 100644 index 0000000..a863005 --- /dev/null +++ b/app/src/main/res/anim/alpha_exit2.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/anim/alpha_no_trans.xml b/app/src/main/res/anim/alpha_no_trans.xml new file mode 100644 index 0000000..582626c --- /dev/null +++ b/app/src/main/res/anim/alpha_no_trans.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/translate_from.xml b/app/src/main/res/anim/translate_from.xml new file mode 100644 index 0000000..7a7360a --- /dev/null +++ b/app/src/main/res/anim/translate_from.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/translate_to.xml b/app/src/main/res/anim/translate_to.xml new file mode 100644 index 0000000..c46c816 --- /dev/null +++ b/app/src/main/res/anim/translate_to.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/icon_back.webp b/app/src/main/res/drawable-xhdpi/icon_back.webp new file mode 100644 index 0000000..dc9ff4d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/icon_back.webp differ diff --git a/app/src/main/res/drawable-xhdpi/icon_back02.webp b/app/src/main/res/drawable-xhdpi/icon_back02.webp new file mode 100644 index 0000000..7893053 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/icon_back02.webp differ diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml new file mode 100644 index 0000000..c8653aa --- /dev/null +++ b/app/src/main/res/drawable/add.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/arrow_down.xml b/app/src/main/res/drawable/arrow_down.xml new file mode 100644 index 0000000..c2eb707 --- /dev/null +++ b/app/src/main/res/drawable/arrow_down.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/arrow_up_right_circle_fill.xml b/app/src/main/res/drawable/arrow_up_right_circle_fill.xml new file mode 100644 index 0000000..bb497cb --- /dev/null +++ b/app/src/main/res/drawable/arrow_up_right_circle_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/background.xml b/app/src/main/res/drawable/background.xml new file mode 100644 index 0000000..1f7bbf7 --- /dev/null +++ b/app/src/main/res/drawable/background.xml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_banner.xml b/app/src/main/res/drawable/bg_banner.xml new file mode 100644 index 0000000..c188ce6 --- /dev/null +++ b/app/src/main/res/drawable/bg_banner.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_bottomsheet.xml b/app/src/main/res/drawable/bg_bottomsheet.xml new file mode 100644 index 0000000..c2e3e01 --- /dev/null +++ b/app/src/main/res/drawable/bg_bottomsheet.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_dialog.xml b/app/src/main/res/drawable/bg_dialog.xml new file mode 100644 index 0000000..8d8876f --- /dev/null +++ b/app/src/main/res/drawable/bg_dialog.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_edittext.xml b/app/src/main/res/drawable/bg_edittext.xml new file mode 100644 index 0000000..dd6eb33 --- /dev/null +++ b/app/src/main/res/drawable/bg_edittext.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_edittext2.xml b/app/src/main/res/drawable/bg_edittext2.xml new file mode 100644 index 0000000..0a501ff --- /dev/null +++ b/app/src/main/res/drawable/bg_edittext2.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_edittext3.xml b/app/src/main/res/drawable/bg_edittext3.xml new file mode 100644 index 0000000..afe7e5e --- /dev/null +++ b/app/src/main/res/drawable/bg_edittext3.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_hometool.xml b/app/src/main/res/drawable/bg_hometool.xml new file mode 100644 index 0000000..c6545fd --- /dev/null +++ b/app/src/main/res/drawable/bg_hometool.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_progress.xml b/app/src/main/res/drawable/bg_progress.xml new file mode 100644 index 0000000..270272e --- /dev/null +++ b/app/src/main/res/drawable/bg_progress.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_round_gray.xml b/app/src/main/res/drawable/bg_round_gray.xml new file mode 100644 index 0000000..fb19145 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_gray.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_round_radius_10.xml b/app/src/main/res/drawable/bg_round_radius_10.xml new file mode 100644 index 0000000..484c0ad --- /dev/null +++ b/app/src/main/res/drawable/bg_round_radius_10.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bookmarks.xml b/app/src/main/res/drawable/bookmarks.xml new file mode 100644 index 0000000..0ba6ac4 --- /dev/null +++ b/app/src/main/res/drawable/bookmarks.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/browsers_outline.xml b/app/src/main/res/drawable/browsers_outline.xml new file mode 100644 index 0000000..24b2a95 --- /dev/null +++ b/app/src/main/res/drawable/browsers_outline.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/check_circle_fill.xml b/app/src/main/res/drawable/check_circle_fill.xml new file mode 100644 index 0000000..31c083a --- /dev/null +++ b/app/src/main/res/drawable/check_circle_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/checkmark_circle.xml b/app/src/main/res/drawable/checkmark_circle.xml new file mode 100644 index 0000000..23e207b --- /dev/null +++ b/app/src/main/res/drawable/checkmark_circle.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/chevron_back.xml b/app/src/main/res/drawable/chevron_back.xml new file mode 100644 index 0000000..9865562 --- /dev/null +++ b/app/src/main/res/drawable/chevron_back.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/chevron_forward.xml b/app/src/main/res/drawable/chevron_forward.xml new file mode 100644 index 0000000..04c0589 --- /dev/null +++ b/app/src/main/res/drawable/chevron_forward.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/clipboard2_minus_fill.xml b/app/src/main/res/drawable/clipboard2_minus_fill.xml new file mode 100644 index 0000000..649b202 --- /dev/null +++ b/app/src/main/res/drawable/clipboard2_minus_fill.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/clipboard_fill.xml b/app/src/main/res/drawable/clipboard_fill.xml new file mode 100644 index 0000000..e1082a4 --- /dev/null +++ b/app/src/main/res/drawable/clipboard_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/close.xml b/app/src/main/res/drawable/close.xml new file mode 100644 index 0000000..ff7e393 --- /dev/null +++ b/app/src/main/res/drawable/close.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/close_outline.xml b/app/src/main/res/drawable/close_outline.xml new file mode 100644 index 0000000..eb17cf7 --- /dev/null +++ b/app/src/main/res/drawable/close_outline.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/dddynamite_1_.xml b/app/src/main/res/drawable/dddynamite_1_.xml new file mode 100644 index 0000000..3146f58 --- /dev/null +++ b/app/src/main/res/drawable/dddynamite_1_.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/delete_circle_fill.xml b/app/src/main/res/drawable/delete_circle_fill.xml new file mode 100644 index 0000000..5909a4b --- /dev/null +++ b/app/src/main/res/drawable/delete_circle_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/desktop.xml b/app/src/main/res/drawable/desktop.xml new file mode 100644 index 0000000..a1c0366 --- /dev/null +++ b/app/src/main/res/drawable/desktop.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/document.xml b/app/src/main/res/drawable/document.xml new file mode 100644 index 0000000..8651f8a --- /dev/null +++ b/app/src/main/res/drawable/document.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/doggie.xml b/app/src/main/res/drawable/doggie.xml new file mode 100644 index 0000000..52d2416 --- /dev/null +++ b/app/src/main/res/drawable/doggie.xml @@ -0,0 +1,624 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml new file mode 100644 index 0000000..4ff873d --- /dev/null +++ b/app/src/main/res/drawable/download.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ellipsis_vertical_outline.xml b/app/src/main/res/drawable/ellipsis_vertical_outline.xml new file mode 100644 index 0000000..35bec13 --- /dev/null +++ b/app/src/main/res/drawable/ellipsis_vertical_outline.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/emoji_sunglasses_fill.xml b/app/src/main/res/drawable/emoji_sunglasses_fill.xml new file mode 100644 index 0000000..bd8b544 --- /dev/null +++ b/app/src/main/res/drawable/emoji_sunglasses_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/emoji_wink_fill.xml b/app/src/main/res/drawable/emoji_wink_fill.xml new file mode 100644 index 0000000..0cda34a --- /dev/null +++ b/app/src/main/res/drawable/emoji_wink_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/end_paper.xml b/app/src/main/res/drawable/end_paper.xml new file mode 100644 index 0000000..d2dcf6c --- /dev/null +++ b/app/src/main/res/drawable/end_paper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/envelope_fill.xml b/app/src/main/res/drawable/envelope_fill.xml new file mode 100644 index 0000000..2ecf9f2 --- /dev/null +++ b/app/src/main/res/drawable/envelope_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/extension_puzzle.xml b/app/src/main/res/drawable/extension_puzzle.xml new file mode 100644 index 0000000..d2df417 --- /dev/null +++ b/app/src/main/res/drawable/extension_puzzle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/folder_fill.xml b/app/src/main/res/drawable/folder_fill.xml new file mode 100644 index 0000000..fb315e4 --- /dev/null +++ b/app/src/main/res/drawable/folder_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/gear_fill.xml b/app/src/main/res/drawable/gear_fill.xml new file mode 100644 index 0000000..6ad227c --- /dev/null +++ b/app/src/main/res/drawable/gear_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/globe.xml b/app/src/main/res/drawable/globe.xml new file mode 100644 index 0000000..4b8bb51 --- /dev/null +++ b/app/src/main/res/drawable/globe.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/drawable/home.xml b/app/src/main/res/drawable/home.xml new file mode 100644 index 0000000..989ab6f --- /dev/null +++ b/app/src/main/res/drawable/home.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/home_outline.xml b/app/src/main/res/drawable/home_outline.xml new file mode 100644 index 0000000..eaf7b56 --- /dev/null +++ b/app/src/main/res/drawable/home_outline.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/homepic1.xml b/app/src/main/res/drawable/homepic1.xml new file mode 100644 index 0000000..32548b3 --- /dev/null +++ b/app/src/main/res/drawable/homepic1.xml @@ -0,0 +1,643 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/hourglass_split.xml b/app/src/main/res/drawable/hourglass_split.xml new file mode 100644 index 0000000..adaff61 --- /dev/null +++ b/app/src/main/res/drawable/hourglass_split.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/house_door_fill.xml b/app/src/main/res/drawable/house_door_fill.xml new file mode 100644 index 0000000..b474d1e --- /dev/null +++ b/app/src/main/res/drawable/house_door_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_bulb.xml b/app/src/main/res/drawable/ic_bulb.xml new file mode 100644 index 0000000..ad6da75 --- /dev/null +++ b/app/src/main/res/drawable/ic_bulb.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_bulb_off.xml b/app/src/main/res/drawable/ic_bulb_off.xml new file mode 100644 index 0000000..edb5836 --- /dev/null +++ b/app/src/main/res/drawable/ic_bulb_off.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..558a981 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000..f4f8689 --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..676aa1b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_scan_view.xml b/app/src/main/res/drawable/ic_scan_view.xml new file mode 100644 index 0000000..e0ab0c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_scan_view.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/main/res/drawable/icon_app3.xml b/app/src/main/res/drawable/icon_app3.xml new file mode 100644 index 0000000..47ae030 --- /dev/null +++ b/app/src/main/res/drawable/icon_app3.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/icon_browser_bookmark.xml b/app/src/main/res/drawable/icon_browser_bookmark.xml new file mode 100644 index 0000000..49d9541 --- /dev/null +++ b/app/src/main/res/drawable/icon_browser_bookmark.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_browser_history.xml b/app/src/main/res/drawable/icon_browser_history.xml new file mode 100644 index 0000000..c415150 --- /dev/null +++ b/app/src/main/res/drawable/icon_browser_history.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/icon_desktop.xml b/app/src/main/res/drawable/icon_desktop.xml new file mode 100644 index 0000000..2c24a3d --- /dev/null +++ b/app/src/main/res/drawable/icon_desktop.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/icon_download.xml b/app/src/main/res/drawable/icon_download.xml new file mode 100644 index 0000000..5e282be --- /dev/null +++ b/app/src/main/res/drawable/icon_download.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/icon_extension.xml b/app/src/main/res/drawable/icon_extension.xml new file mode 100644 index 0000000..82d2cba --- /dev/null +++ b/app/src/main/res/drawable/icon_extension.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/icon_extensions.xml b/app/src/main/res/drawable/icon_extensions.xml new file mode 100644 index 0000000..6a8696b --- /dev/null +++ b/app/src/main/res/drawable/icon_extensions.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/icon_folder3.xml b/app/src/main/res/drawable/icon_folder3.xml new file mode 100644 index 0000000..71cb6a6 --- /dev/null +++ b/app/src/main/res/drawable/icon_folder3.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_music3.xml b/app/src/main/res/drawable/icon_music3.xml new file mode 100644 index 0000000..1dfa83c --- /dev/null +++ b/app/src/main/res/drawable/icon_music3.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/icon_pic3.xml b/app/src/main/res/drawable/icon_pic3.xml new file mode 100644 index 0000000..498b14b --- /dev/null +++ b/app/src/main/res/drawable/icon_pic3.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_privacy.xml b/app/src/main/res/drawable/icon_privacy.xml new file mode 100644 index 0000000..99edec8 --- /dev/null +++ b/app/src/main/res/drawable/icon_privacy.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/icon_privacy_fill.xml b/app/src/main/res/drawable/icon_privacy_fill.xml new file mode 100644 index 0000000..84a0b46 --- /dev/null +++ b/app/src/main/res/drawable/icon_privacy_fill.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/drawable/icon_refresh.xml b/app/src/main/res/drawable/icon_refresh.xml new file mode 100644 index 0000000..09ff7fc --- /dev/null +++ b/app/src/main/res/drawable/icon_refresh.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/icon_setting6.xml b/app/src/main/res/drawable/icon_setting6.xml new file mode 100644 index 0000000..c822788 --- /dev/null +++ b/app/src/main/res/drawable/icon_setting6.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/icon_setting_more.xml b/app/src/main/res/drawable/icon_setting_more.xml new file mode 100644 index 0000000..13b1096 --- /dev/null +++ b/app/src/main/res/drawable/icon_setting_more.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_txt2.xml b/app/src/main/res/drawable/icon_txt2.xml new file mode 100644 index 0000000..dc3da3c --- /dev/null +++ b/app/src/main/res/drawable/icon_txt2.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_unknown.xml b/app/src/main/res/drawable/icon_unknown.xml new file mode 100644 index 0000000..6e5d5ae --- /dev/null +++ b/app/src/main/res/drawable/icon_unknown.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/icon_video2.xml b/app/src/main/res/drawable/icon_video2.xml new file mode 100644 index 0000000..46190bf --- /dev/null +++ b/app/src/main/res/drawable/icon_video2.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_web_refresh.xml b/app/src/main/res/drawable/icon_web_refresh.xml new file mode 100644 index 0000000..2f8538a --- /dev/null +++ b/app/src/main/res/drawable/icon_web_refresh.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/icon_zip2.xml b/app/src/main/res/drawable/icon_zip2.xml new file mode 100644 index 0000000..35d6a58 --- /dev/null +++ b/app/src/main/res/drawable/icon_zip2.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/lock_fill.xml b/app/src/main/res/drawable/lock_fill.xml new file mode 100644 index 0000000..aec743c --- /dev/null +++ b/app/src/main/res/drawable/lock_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/logo72.xml b/app/src/main/res/drawable/logo72.xml new file mode 100644 index 0000000..15c94b9 --- /dev/null +++ b/app/src/main/res/drawable/logo72.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/logo_round.xml b/app/src/main/res/drawable/logo_round.xml new file mode 100644 index 0000000..be61b05 --- /dev/null +++ b/app/src/main/res/drawable/logo_round.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/menu_outline.xml b/app/src/main/res/drawable/menu_outline.xml new file mode 100644 index 0000000..d46c1cb --- /dev/null +++ b/app/src/main/res/drawable/menu_outline.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/pause_circle.xml b/app/src/main/res/drawable/pause_circle.xml new file mode 100644 index 0000000..83fa065 --- /dev/null +++ b/app/src/main/res/drawable/pause_circle.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/pc_display.xml b/app/src/main/res/drawable/pc_display.xml new file mode 100644 index 0000000..de5c65c --- /dev/null +++ b/app/src/main/res/drawable/pc_display.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/pencil_square.xml b/app/src/main/res/drawable/pencil_square.xml new file mode 100644 index 0000000..7086943 --- /dev/null +++ b/app/src/main/res/drawable/pencil_square.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/people_circle.xml b/app/src/main/res/drawable/people_circle.xml new file mode 100644 index 0000000..505ea5c --- /dev/null +++ b/app/src/main/res/drawable/people_circle.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/person_circle.xml b/app/src/main/res/drawable/person_circle.xml new file mode 100644 index 0000000..5f614f4 --- /dev/null +++ b/app/src/main/res/drawable/person_circle.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/phone_fill.xml b/app/src/main/res/drawable/phone_fill.xml new file mode 100644 index 0000000..6761e44 --- /dev/null +++ b/app/src/main/res/drawable/phone_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/play_circle.xml b/app/src/main/res/drawable/play_circle.xml new file mode 100644 index 0000000..7448c03 --- /dev/null +++ b/app/src/main/res/drawable/play_circle.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/qq.xml b/app/src/main/res/drawable/qq.xml new file mode 100644 index 0000000..b122bd4 --- /dev/null +++ b/app/src/main/res/drawable/qq.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/qr_code.xml b/app/src/main/res/drawable/qr_code.xml new file mode 100644 index 0000000..3b9b73e --- /dev/null +++ b/app/src/main/res/drawable/qr_code.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/qr_code_outline.xml b/app/src/main/res/drawable/qr_code_outline.xml new file mode 100644 index 0000000..438a7b2 --- /dev/null +++ b/app/src/main/res/drawable/qr_code_outline.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/reload.xml b/app/src/main/res/drawable/reload.xml new file mode 100644 index 0000000..9e68cf5 --- /dev/null +++ b/app/src/main/res/drawable/reload.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/rose.xml b/app/src/main/res/drawable/rose.xml new file mode 100644 index 0000000..88df76c --- /dev/null +++ b/app/src/main/res/drawable/rose.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/rose_outline.xml b/app/src/main/res/drawable/rose_outline.xml new file mode 100644 index 0000000..6d28ee4 --- /dev/null +++ b/app/src/main/res/drawable/rose_outline.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/search.xml b/app/src/main/res/drawable/search.xml new file mode 100644 index 0000000..28a3c9f --- /dev/null +++ b/app/src/main/res/drawable/search.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/send_fill.xml b/app/src/main/res/drawable/send_fill.xml new file mode 100644 index 0000000..1923d74 --- /dev/null +++ b/app/src/main/res/drawable/send_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/shape_bottom_border.xml b/app/src/main/res/drawable/shape_bottom_border.xml new file mode 100644 index 0000000..8e562a5 --- /dev/null +++ b/app/src/main/res/drawable/shape_bottom_border.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_corners_top.xml b/app/src/main/res/drawable/shape_corners_top.xml new file mode 100644 index 0000000..877b27c --- /dev/null +++ b/app/src/main/res/drawable/shape_corners_top.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_corners_top_grey.xml b/app/src/main/res/drawable/shape_corners_top_grey.xml new file mode 100644 index 0000000..3ae5f20 --- /dev/null +++ b/app/src/main/res/drawable/shape_corners_top_grey.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_dialog_cardbg.xml b/app/src/main/res/drawable/shape_dialog_cardbg.xml new file mode 100644 index 0000000..47f0c89 --- /dev/null +++ b/app/src/main/res/drawable/shape_dialog_cardbg.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_top_border_radius.xml b/app/src/main/res/drawable/shape_top_border_radius.xml new file mode 100644 index 0000000..cacd016 --- /dev/null +++ b/app/src/main/res/drawable/shape_top_border_radius.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/share_fill.xml b/app/src/main/res/drawable/share_fill.xml new file mode 100644 index 0000000..824aaee --- /dev/null +++ b/app/src/main/res/drawable/share_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/shield_fill.xml b/app/src/main/res/drawable/shield_fill.xml new file mode 100644 index 0000000..e877a2d --- /dev/null +++ b/app/src/main/res/drawable/shield_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/shield_slash_fill.xml b/app/src/main/res/drawable/shield_slash_fill.xml new file mode 100644 index 0000000..ca484f3 --- /dev/null +++ b/app/src/main/res/drawable/shield_slash_fill.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/square.xml b/app/src/main/res/drawable/square.xml new file mode 100644 index 0000000..57119e1 --- /dev/null +++ b/app/src/main/res/drawable/square.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/square_outline.xml b/app/src/main/res/drawable/square_outline.xml new file mode 100644 index 0000000..bba8459 --- /dev/null +++ b/app/src/main/res/drawable/square_outline.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/sssurf_2_.xml b/app/src/main/res/drawable/sssurf_2_.xml new file mode 100644 index 0000000..d73243f --- /dev/null +++ b/app/src/main/res/drawable/sssurf_2_.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/star.xml b/app/src/main/res/drawable/star.xml new file mode 100644 index 0000000..5bd8f1d --- /dev/null +++ b/app/src/main/res/drawable/star.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/star_fill.xml b/app/src/main/res/drawable/star_fill.xml new file mode 100644 index 0000000..8affe6c --- /dev/null +++ b/app/src/main/res/drawable/star_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/sync_circle.xml b/app/src/main/res/drawable/sync_circle.xml new file mode 100644 index 0000000..db6f3ed --- /dev/null +++ b/app/src/main/res/drawable/sync_circle.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/telegram.xml b/app/src/main/res/drawable/telegram.xml new file mode 100644 index 0000000..bf1b899 --- /dev/null +++ b/app/src/main/res/drawable/telegram.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/trash3_fill.xml b/app/src/main/res/drawable/trash3_fill.xml new file mode 100644 index 0000000..41aaaeb --- /dev/null +++ b/app/src/main/res/drawable/trash3_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/unlock_fill.xml b/app/src/main/res/drawable/unlock_fill.xml new file mode 100644 index 0000000..0ebc70a --- /dev/null +++ b/app/src/main/res/drawable/unlock_fill.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/wechat.xml b/app/src/main/res/drawable/wechat.xml new file mode 100644 index 0000000..35b45b7 --- /dev/null +++ b/app/src/main/res/drawable/wechat.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/wechat_qr.jpg b/app/src/main/res/drawable/wechat_qr.jpg new file mode 100644 index 0000000..44b79de Binary files /dev/null and b/app/src/main/res/drawable/wechat_qr.jpg differ diff --git a/app/src/main/res/drawable/wechat_qr_hk.jpg b/app/src/main/res/drawable/wechat_qr_hk.jpg new file mode 100644 index 0000000..fca4d33 Binary files /dev/null and b/app/src/main/res/drawable/wechat_qr_hk.jpg differ diff --git a/app/src/main/res/font/alatsi.xml b/app/src/main/res/font/alatsi.xml new file mode 100644 index 0000000..9124cfd --- /dev/null +++ b/app/src/main/res/font/alatsi.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/alfa_slab_one.xml b/app/src/main/res/font/alfa_slab_one.xml new file mode 100644 index 0000000..52b8208 --- /dev/null +++ b/app/src/main/res/font/alfa_slab_one.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/baloo_bhaina.xml b/app/src/main/res/font/baloo_bhaina.xml new file mode 100644 index 0000000..89fe3ee --- /dev/null +++ b/app/src/main/res/font/baloo_bhaina.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/goblin_one.xml b/app/src/main/res/font/goblin_one.xml new file mode 100644 index 0000000..583c0b8 --- /dev/null +++ b/app/src/main/res/font/goblin_one.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/font/roboto_bold.xml b/app/src/main/res/font/roboto_bold.xml new file mode 100644 index 0000000..1e8c20a --- /dev/null +++ b/app/src/main/res/font/roboto_bold.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout-large/activity_main.xml b/app/src/main/res/layout-large/activity_main.xml new file mode 100644 index 0000000..381fd06 --- /dev/null +++ b/app/src/main/res/layout-large/activity_main.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large/content_main.xml b/app/src/main/res/layout-large/content_main.xml new file mode 100644 index 0000000..3e6a59d --- /dev/null +++ b/app/src/main/res/layout-large/content_main.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large/content_url.xml b/app/src/main/res/layout-large/content_url.xml new file mode 100644 index 0000000..eff1bda --- /dev/null +++ b/app/src/main/res/layout-large/content_url.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large/fragment_first.xml b/app/src/main/res/layout-large/fragment_first.xml new file mode 100644 index 0000000..e5a5322 --- /dev/null +++ b/app/src/main/res/layout-large/fragment_first.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large/item_menu_addons.xml b/app/src/main/res/layout-large/item_menu_addons.xml new file mode 100644 index 0000000..983d932 --- /dev/null +++ b/app/src/main/res/layout-large/item_menu_addons.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large/item_tablist_phone.xml b/app/src/main/res/layout-large/item_tablist_phone.xml new file mode 100644 index 0000000..f393207 --- /dev/null +++ b/app/src/main/res/layout-large/item_tablist_phone.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_file_browser.xml b/app/src/main/res/layout/activity_file_browser.xml new file mode 100644 index 0000000..90d5977 --- /dev/null +++ b/app/src/main/res/layout/activity_file_browser.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_holder.xml b/app/src/main/res/layout/activity_holder.xml new file mode 100644 index 0000000..94a9d31 --- /dev/null +++ b/app/src/main/res/layout/activity_holder.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ff26343 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_holder.xml b/app/src/main/res/layout/content_holder.xml new file mode 100644 index 0000000..035f1a0 --- /dev/null +++ b/app/src/main/res/layout/content_holder.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..2a54765 --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_download_notification.xml b/app/src/main/res/layout/custom_download_notification.xml new file mode 100644 index 0000000..bec1099 --- /dev/null +++ b/app/src/main/res/layout/custom_download_notification.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dia_bookmark.xml b/app/src/main/res/layout/dia_bookmark.xml new file mode 100644 index 0000000..6f905c4 --- /dev/null +++ b/app/src/main/res/layout/dia_bookmark.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dia_choice.xml b/app/src/main/res/layout/dia_choice.xml new file mode 100644 index 0000000..dfd4117 --- /dev/null +++ b/app/src/main/res/layout/dia_choice.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dia_contextmenu.xml b/app/src/main/res/layout/dia_contextmenu.xml new file mode 100644 index 0000000..e318a80 --- /dev/null +++ b/app/src/main/res/layout/dia_contextmenu.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dia_engine_select.xml b/app/src/main/res/layout/dia_engine_select.xml new file mode 100644 index 0000000..6617359 --- /dev/null +++ b/app/src/main/res/layout/dia_engine_select.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dia_install.xml b/app/src/main/res/layout/dia_install.xml new file mode 100644 index 0000000..e6f5d3c --- /dev/null +++ b/app/src/main/res/layout/dia_install.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml new file mode 100644 index 0000000..1da5cd2 --- /dev/null +++ b/app/src/main/res/layout/fragment_about.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_addons.xml b/app/src/main/res/layout/fragment_addons.xml new file mode 100644 index 0000000..229266a --- /dev/null +++ b/app/src/main/res/layout/fragment_addons.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_addons_manager.xml b/app/src/main/res/layout/fragment_addons_manager.xml new file mode 100644 index 0000000..8daca0a --- /dev/null +++ b/app/src/main/res/layout/fragment_addons_manager.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + +