diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml new file mode 100644 index 00000000..f9b6f624 --- /dev/null +++ b/.github/workflows/android-build.yml @@ -0,0 +1,42 @@ +name: Android Build + +on: + pull_request: + branches: [ "**" ] + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Create local.properties + run: | + echo "MIXPANEL_KEY=${{ secrets.MIXPANEL_KEY }}" >> local.properties + echo "STOREFILE=${{ secrets.STOREFILE }}" >> local.properties + echo "STOREPASSWORD=${{ secrets.STOREPASSWORD }}" >> local.properties + echo "KEYALIAS=${{ secrets.KEYALIAS }}" >> local.properties + echo "KEYPASSWORD=${{ secrets.KEYPASSWORD }}" >> local.properties + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Build and Test + uses: gradle/gradle-build-action@v3 + with: + arguments: | + assembleDebug + assembleRelease + lintDebug + lintRelease + test + cache-read-only: false \ No newline at end of file diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml index 89690488..1bef5e74 100644 --- a/.github/workflows/detekt.yml +++ b/.github/workflows/detekt.yml @@ -16,6 +16,7 @@ on: jobs: detekt: + concurrency: detekt-${{ github.ref }} name: Static Code Analysis with Detekt runs-on: ubuntu-latest diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6457c7c4..6689d091 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,10 +64,29 @@ android { compose = true } + signingConfigs { + create("release") { + val props = loadLocalProperties() + storeFile = file(props["STOREFILE"] as? String ?: "") + storePassword = props["STOREPASSWORD"] as? String ?: "" + keyAlias = props["KEYALIAS"] as? String ?: "" + keyPassword = props["KEYPASSWORD"] as? String ?: "" + } + + getByName("debug") { + val props = loadLocalProperties() + storeFile = file(props["STOREFILE"] as? String ?: "") + storeFile = props["STOREFILE"]?.let { file(it) } + storePassword = props["STOREPASSWORD"] as? String ?: "" + keyAlias = props["KEYALIAS"] as? String ?: "" + keyPassword = props["KEYPASSWORD"] as? String ?: "" + } + } + buildTypes { getByName("release") { - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.getByName("release") isMinifyEnabled = false isShrinkResources = false applicationIdSuffix = ".release" @@ -78,16 +97,7 @@ android { signingConfig = signingConfigs.getByName("debug") applicationIdSuffix = ".debug" isMinifyEnabled = false - } - } - - signingConfigs { - getByName("debug") { - val props = loadLocalProperties() - storeFile = file(props["STOREFILE"] as? String ?: "") - storePassword = props["STOREPASSWORD"] as? String ?: "" - keyAlias = props["KEYALIAS"] as? String ?: "" - keyPassword = props["KEYPASSWORD"] as? String ?: "" + buildConfigField("Boolean", "ENABLE_BUILD_CACHE", "true") } } @@ -241,9 +251,9 @@ dependencies { implementation("com.google.api-client:google-api-client-android:1.26.0") implementation("com.google.apis:google-api-services-drive:v3-rev136-1.25.0") - // Tor Libraries - implementation(libs.tor.android) - implementation(libs.jtorctl) + // Internal Tor Libraries + //implementation(libs.tor.android) + //implementation(libs.jtorctl) implementation(libs.bitcoinj.core) implementation("com.eclipsesource.j2v8:j2v8:6.2.1@aar") @@ -275,7 +285,7 @@ dependencies { implementation("com.github.derlio:audio-waveform:v1.0.1") implementation(libs.clean.insights) - implementation(libs.netcipher) + implementation(fileTree("libs")) // Mixpanel analytics implementation(libs.mixpanel) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index e46e32ce..aa96555c 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -389,7 +389,6 @@ FinalNewline:VideoRequestHandler.kt$net.opendasharchive.openarchive.fragments.VideoRequestHandler.kt FinalNewline:ViewExtension.kt$net.opendasharchive.openarchive.extensions.ViewExtension.kt FinalNewline:WebDAVModel.kt$net.opendasharchive.openarchive.db.WebDAVModel.kt - FinalNewline:WebDavActivity.kt$net.opendasharchive.openarchive.services.webdav.WebDavActivity.kt FinalNewline:WebDavConduit.kt$net.opendasharchive.openarchive.services.webdav.WebDavConduit.kt FinalNewline:WebDavFragment.kt$net.opendasharchive.openarchive.services.webdav.WebDavFragment.kt FinalNewline:WebDavSetupLicenseFragment.kt$net.opendasharchive.openarchive.services.webdav.WebDavSetupLicenseFragment.kt diff --git a/app/libs/netcipher.aar b/app/libs/netcipher.aar new file mode 100644 index 00000000..a7543670 Binary files /dev/null and b/app/libs/netcipher.aar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 990ca5c9..55a5cc9a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -105,7 +105,8 @@ android:name=".features.main.HomeActivity" android:exported="true" android:screenOrientation="portrait" - android:theme="@style/SaveAppTheme.NoActionBar"> + android:theme="@style/SaveAppTheme.NoActionBar" + tools:ignore="DiscouragedApi"> @@ -142,44 +143,10 @@ - - - - - - - - - - - - - - - - - - - - - + android:foregroundServiceType="dataSync" /> diff --git a/app/src/main/java/info/guardianproject/netcipher/client/StrongOkHttpClientBuilder.java b/app/src/main/java/info/guardianproject/netcipher/client/StrongOkHttpClientBuilder.java new file mode 100644 index 00000000..dea2fe0b --- /dev/null +++ b/app/src/main/java/info/guardianproject/netcipher/client/StrongOkHttpClientBuilder.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * Copyright 2015 str4d + * Portions Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 info.guardianproject.netcipher.client; + +import android.content.Context; +import android.content.Intent; +import javax.net.ssl.SSLSocketFactory; + +import okhttp3.OkHttpClient; +import okhttp3.Request; + +/** + * Creates an OkHttpClient using NetCipher configuration. Use + * build() if you have no other OkHttpClient configuration + * that you need to perform. Or, use applyTo() to augment an + * existing OkHttpClient.Builder with NetCipher. + */ +public class StrongOkHttpClientBuilder extends + StrongBuilderBase { + /** + * Creates a StrongOkHttpClientBuilder using the strongest set + * of options for security. Use this if the strongest set of + * options is what you want; otherwise, create a + * builder via the constructor and configure it as you see fit. + * + * @param context any Context will do + * @return a configured StrongOkHttpClientBuilder + * @throws Exception + */ + static public StrongOkHttpClientBuilder forMaxSecurity(Context context) + throws Exception { + return(new StrongOkHttpClientBuilder(context) + .withBestProxy()); + } + + /** + * Creates a builder instance. + * + * @param context any Context will do; builder will hold onto + * Application context + */ + public StrongOkHttpClientBuilder(Context context) { + super(context); + } + + /** + * Copy constructor. + * + * @param original builder to clone + */ + public StrongOkHttpClientBuilder(StrongOkHttpClientBuilder original) { + super(original); + } + + /** + * OkHttp3 does not support SOCKS proxies: + * https://github.com/square/okhttp/issues/2315 + * + * @return false + */ + @Override + public boolean supportsSocksProxy() { + return(false); + } + + /** + * {@inheritDoc} + */ + @Override + public OkHttpClient build(Intent status) { + return(applyTo(new OkHttpClient.Builder(), status).build()); + } + + /** + * Adds NetCipher configuration to an existing OkHttpClient.Builder, + * in case you have additional configuration that you wish to + * perform. + * + * @param builder a new or partially-configured OkHttpClient.Builder + * @return the same builder + */ + public OkHttpClient.Builder applyTo(OkHttpClient.Builder builder, Intent status) { + SSLSocketFactory factory=buildSocketFactory(); + + if (factory!=null) { + builder.sslSocketFactory(factory); + } + + return(builder + .proxy(buildProxy(status))); + } + + @Override + protected String get(Intent status, OkHttpClient connection, + String url) throws Exception { + Request request=new Request.Builder().url(TOR_CHECK_URL).build(); + + return(connection.newCall(request).execute().body().string()); + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt index fd0e7d09..b1b37769 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt @@ -22,7 +22,6 @@ import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.features.settings.passcode.PasscodeManager import net.opendasharchive.openarchive.util.Analytics import net.opendasharchive.openarchive.util.Prefs -import net.opendasharchive.openarchive.util.Theme import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin @@ -69,7 +68,7 @@ class SaveApp : SugarApp(), SingletonImageLoader.Factory { Prefs.load(this) applyTheme() - if (Prefs.useTor) initNetCipher() + initNetCipher() CleanInsightsManager.init(this) @@ -78,13 +77,17 @@ class SaveApp : SugarApp(), SingletonImageLoader.Factory { private fun initNetCipher() { AppLogger.d("Initializing NetCipher client") - val oh = OrbotHelper.get(this) - if (BuildConfig.DEBUG) { - oh.skipOrbotValidation() + OrbotHelper.get(this).apply { + if (BuildConfig.DEBUG) { + skipOrbotValidation() + } + init() } -// oh.init() + if (Prefs.useTor) { + OrbotHelper.requestStartTor(this@SaveApp) + } } private fun createSnowbirdNotificationChannel() { diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt index d3156218..ff9d46ab 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt @@ -1,14 +1,12 @@ package net.opendasharchive.openarchive.core.di -import android.content.Context -import com.google.api.services.drive.Drive -import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine -import net.opendasharchive.openarchive.features.core.dialog.DefaultResourceProvider import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager import net.opendasharchive.openarchive.features.core.dialog.ResourceProvider +import net.opendasharchive.openarchive.features.core.dialog.DefaultResourceProvider import net.opendasharchive.openarchive.features.folders.BrowseFoldersViewModel -import net.opendasharchive.openarchive.features.main.MainViewModel import net.opendasharchive.openarchive.features.main.ui.HomeViewModel +import net.opendasharchive.openarchive.features.main.MainViewModel +import net.opendasharchive.openarchive.services.servicesModule import org.koin.android.ext.koin.androidApplication import org.koin.core.module.dsl.viewModel import org.koin.dsl.module @@ -27,10 +25,9 @@ val coreModule = module { } viewModel { - BrowseFoldersViewModel( - context = get() - ) + BrowseFoldersViewModel(get()) } + includes(servicesModule) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt b/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt index 1ad6c532..5b82be12 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt @@ -10,17 +10,16 @@ import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -suspend fun OkHttpClient.enqueueResult( - request: Request, - onResume: (Response) -> T -) = suspendCancellableCoroutine { continuation -> +suspend fun OkHttpClient.enqueueResult( + request: Request +): Result = suspendCancellableCoroutine { continuation -> newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { continuation.resumeWithException(e) } override fun onResponse(call: Call, response: Response) { - continuation.resume(onResume(response)) + continuation.resume(Result.success(response)) } }) diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt b/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt index 562c287f..5b5de92b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt @@ -2,17 +2,14 @@ package net.opendasharchive.openarchive.db import android.content.Context import android.content.Intent -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity import com.github.abdularis.civ.AvatarImageView import com.orm.SugarRecord import net.opendasharchive.openarchive.R @@ -20,7 +17,6 @@ import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity import net.opendasharchive.openarchive.services.gdrive.GDriveConduit import net.opendasharchive.openarchive.services.internetarchive.IaConduit -import net.opendasharchive.openarchive.util.DrawableUtil import net.opendasharchive.openarchive.util.Prefs import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @@ -76,10 +72,6 @@ data class Space( RAVEN(5, "DWeb Service"), } - enum class IconStyle { - SOLID, OUTLINE - } - companion object { fun getAll(): Iterator { return findAll(Space::class.java) @@ -131,6 +123,15 @@ data class Space( activity.startActivity(Intent(activity, SpaceSetupActivity::class.java)) } } + + fun navigate(activity: FragmentActivity) { + if (getAll().hasNext()) { + activity.finish() + } else { + activity.finishAffinity() + activity.startActivity(Intent(activity, SpaceSetupActivity::class.java)) + } + } } val friendlyName: String @@ -151,7 +152,7 @@ data class Space( var tType: Type get() = Type.entries.first { it.id == type } set(value) { - type = (value ?: Type.WEBDAV).id + type = value.id } var license: String? @@ -199,45 +200,30 @@ data class Space( "space_id = ? AND description = ?", id.toString(), description - ).size > 0 + ).isNotEmpty() } - fun getAvatar(context: Context, style: IconStyle = IconStyle.SOLID): Drawable? { - - - return when (tType) { + fun getAvatar(context: Context): Drawable? = when (tType) { Type.WEBDAV -> ContextCompat.getDrawable( context, R.drawable.ic_private_server - ) // ?.tint(color) + ) Type.INTERNET_ARCHIVE -> ContextCompat.getDrawable( context, R.drawable.ic_internet_archive - ) // ?.tint(color) + ) Type.GDRIVE -> ContextCompat.getDrawable( context, R.drawable.logo_gdrive_outline - ) // ?.tint(color) - - Type.RAVEN -> ContextCompat.getDrawable(context, R.drawable.snowbird) // ?.tint(color) - - else -> { - val color = ContextCompat.getColor(context, R.color.colorOnBackground) - BitmapDrawable( - context.resources, - DrawableUtil.createCircularTextDrawable(initial, color) - ) - } + ) - } + Type.RAVEN -> ContextCompat.getDrawable(context, R.drawable.snowbird) } @Composable - fun getAvatar(): Painter { - - return when (tType) { + fun getAvatar(): Painter = when (tType) { Type.WEBDAV -> painterResource(R.drawable.ic_space_private_server) Type.INTERNET_ARCHIVE -> painterResource(R.drawable.ic_space_interent_archive) @@ -245,15 +231,8 @@ data class Space( Type.GDRIVE -> painterResource(R.drawable.logo_gdrive_outline) Type.RAVEN -> painterResource(R.drawable.ic_space_dweb) - null -> { - val context = LocalContext.current - val color = ContextCompat.getColor(context, R.color.colorOnBackground) - val bitmap = DrawableUtil.createCircularTextDrawable(initial, color) - val imageBitmap = bitmap.asImageBitmap() - BitmapPainter(imageBitmap) - } } - } + fun setAvatar(view: ImageView) { when (tType) { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt index 4f49271a..63699944 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/BaseActivity.kt @@ -6,6 +6,7 @@ import android.view.ViewGroup import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import com.google.android.material.appbar.MaterialToolbar import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme @@ -40,6 +41,7 @@ abstract class BaseActivity : AppCompatActivity() { // Add ComposeView if not already present if (rootView.findViewById(R.id.compose_dialog_host) == null) { ComposeView(this).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) id = R.id.compose_dialog_host layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/core/NavArgument.kt b/app/src/main/java/net/opendasharchive/openarchive/features/core/NavArgument.kt new file mode 100644 index 00000000..ca16436d --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/core/NavArgument.kt @@ -0,0 +1,9 @@ +package net.opendasharchive.openarchive.features.core + +object NavArgument { + const val START_DESTINATION = "start_destination" + const val SPACE_ID = "space_id" + const val FOLDER_ID = "folder_id" + const val FOLDER_NAME = "folder_name" + const val SHOW_ARCHIVED_FOLDERS = "show_archived_folders" +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt deleted file mode 100644 index f04c5ee0..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderActivity.kt +++ /dev/null @@ -1,105 +0,0 @@ -package net.opendasharchive.openarchive.features.folders - -import android.content.Intent -import android.os.Bundle -import android.view.MenuItem -import androidx.activity.compose.setContent -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme -import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.core.BaseActivity -import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity - -class AddFolderActivity : BaseActivity() { - - companion object { - const val EXTRA_FOLDER_ID = "folder_id" - const val EXTRA_FOLDER_NAME = "folder_name" - } - - private lateinit var mResultLauncher: ActivityResultLauncher - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - mResultLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - setResult(RESULT_OK, it.data) - finish() - } else { - val name = it.data?.getStringExtra(EXTRA_FOLDER_NAME) - - if (!name.isNullOrBlank()) { - val i = Intent(this, CreateNewFolderFragment::class.java) - i.putExtra(EXTRA_FOLDER_NAME, name) - - mResultLauncher.launch(i) - } - } - } - - //mBinding = ActivityAddFolderBinding.inflate(layoutInflater) - //setContentView(mBinding.root) - - - setContent { - - SaveAppTheme { - - AddFolderScreen( -// onCreateFolder = { -// setFolder(browse = false) -// }, -// onBrowseFolders = { -// setFolder(browse = true) -// }, -// onNavigateBack = { -// finish() -// } - ) - } - } - - - - - // We cannot browse the Internet Archive. Directly forward to creating a project, - // as it doesn't make sense to show a one-option menu. - if (Space.current?.tType == Space.Type.INTERNET_ARCHIVE) { - //mBinding.browseFolderContainer.hide() - - finish() - setFolder(false) - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - finish() - - return true - } - } - - return super.onOptionsItemSelected(item) - } - - private fun setFolder(browse: Boolean) { - if (Space.current == null) { - finish() - startActivity(Intent(this, SpaceSetupActivity::class.java)) - - return - } - - mResultLauncher.launch( - Intent( - this, - if (browse) BrowseFoldersFragment::class.java else CreateNewFolderFragment::class.java - ) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt index f7dd6932..04869121 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/AddFolderScreen.kt @@ -43,6 +43,8 @@ import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme +// This screen is used in space_setup_navigation.xml nav graph +@Suppress("unused") @Composable fun AddFolderScreen() { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFolderScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFolderScreen.kt index 67dd1470..930c82c5 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFolderScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFolderScreen.kt @@ -27,6 +27,7 @@ import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.DefaultScaffoldPreview import org.koin.androidx.compose.koinViewModel import java.util.Date +import net.opendasharchive.openarchive.features.folders.BrowseFoldersViewModel.Folder @Composable fun BrowseFolderScreen( diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt index 699fb3ca..e14a5f8c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersAdapter.kt @@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.FolderRowBinding +import net.opendasharchive.openarchive.features.folders.BrowseFoldersViewModel.Folder import java.text.SimpleDateFormat class BrowseFoldersAdapter( diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt index 042c8a81..181c5a2f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersFragment.kt @@ -1,6 +1,5 @@ package net.opendasharchive.openarchive.features.folders -import android.app.Activity import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle @@ -18,7 +17,9 @@ import net.opendasharchive.openarchive.databinding.FragmentBrowseFoldersBinding import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseFragment +import net.opendasharchive.openarchive.features.core.NavArgument import net.opendasharchive.openarchive.features.core.dialog.showSuccessDialog +import net.opendasharchive.openarchive.features.folders.BrowseFoldersViewModel.Folder import net.opendasharchive.openarchive.util.extensions.toggle import org.koin.androidx.viewmodel.ext.android.viewModel import java.util.Date @@ -98,7 +99,7 @@ class BrowseFoldersFragment : BaseFragment(), MenuProvider { private fun navigateBackWithResult(projectId: Long) { requireActivity().setResult(RESULT_OK, Intent().apply { - putExtra(AddFolderActivity.EXTRA_FOLDER_ID, projectId) + putExtra(NavArgument.FOLDER_ID, projectId) }) requireActivity().finish() } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt index 0f1055df..de2eae3d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/BrowseFoldersViewModel.kt @@ -11,18 +11,21 @@ import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.services.SaveClient import net.opendasharchive.openarchive.services.gdrive.GDriveConduit +import org.koin.java.KoinJavaComponent.inject import timber.log.Timber import java.io.IOException import java.util.Date -data class Folder(val name: String, val modified: Date) - class BrowseFoldersViewModel(private val context: Context) : ViewModel() { + data class Folder(val name: String, val modified: Date) + private val mFolders = MutableLiveData>() + private val client: SaveClient by inject(SaveClient::class.java) + val folders: LiveData> get() = mFolders @@ -35,7 +38,7 @@ class BrowseFoldersViewModel(private val context: Context) : ViewModel() { try { val value = withContext(Dispatchers.IO) { when (space.tType) { - Space.Type.WEBDAV -> getWebDavFolders(context, space) + Space.Type.WEBDAV -> getWebDavFolders(space) Space.Type.GDRIVE -> getGDriveFolders(context, space) @@ -57,10 +60,10 @@ class BrowseFoldersViewModel(private val context: Context) : ViewModel() { } @Throws(IOException::class) - private suspend fun getWebDavFolders(context: Context, space: Space): List { + private fun getWebDavFolders(space: Space): List { val root = space.hostUrl?.encodedPath - return SaveClient.getSardine(context, space).list(space.host)?.mapNotNull { + return client.webdav(space).list(space.host)?.mapNotNull { if (it?.isDirectory == true && it.path != root) { Folder(it.name, it.modified ?: Date()) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt index 7386f33b..08b48214 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/folders/CreateNewFolderFragment.kt @@ -18,6 +18,7 @@ import net.opendasharchive.openarchive.databinding.FragmentCreateNewFolderBindin import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseFragment +import net.opendasharchive.openarchive.features.core.NavArgument import net.opendasharchive.openarchive.features.core.dialog.showSuccessDialog import net.opendasharchive.openarchive.features.settings.CreativeCommonsLicenseManager import net.opendasharchive.openarchive.util.extensions.hide @@ -45,7 +46,7 @@ class CreateNewFolderFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) val intent = requireActivity().intent - binding.newFolder.setText(intent.getStringExtra(AddFolderActivity.EXTRA_FOLDER_NAME)) + binding.newFolder.setText(intent.getStringExtra(NavArgument.FOLDER_NAME)) binding.newFolder.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { @@ -142,7 +143,7 @@ class CreateNewFolderFragment : BaseFragment() { private fun navigateBackWithResult(projectId: Long) { val i = Intent() - i.putExtra(AddFolderActivity.EXTRA_FOLDER_ID, projectId) + i.putExtra(NavArgument.FOLDER_ID, projectId) requireActivity().setResult(RESULT_OK, i) requireActivity().finish() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt index 3e1bb47e..1ffb77eb 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -1,7 +1,5 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource -import android.content.Context -import net.opendasharchive.openarchive.core.infrastructure.client.enqueueResult import net.opendasharchive.openarchive.features.internetarchive.InternetArchiveGson import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest @@ -14,11 +12,11 @@ import okhttp3.Request private const val LOGIN_URI = "https://archive.org/services/xauthn?op=login" class InternetArchiveRemoteSource( - private val context: Context, + private val client: SaveClient, private val gson: InternetArchiveGson ) { suspend fun login(request: InternetArchiveLoginRequest): Result = - SaveClient.get(context).enqueueResult( + client.enqueue( Request.Builder() .url(LOGIN_URI) .post( @@ -27,22 +25,21 @@ class InternetArchiveRemoteSource( .add("password", request.password).build() ) .build() - ) { response -> - val data = gson.fromJson( + ).map { response -> + gson.fromJson( response.body?.string(), InternetArchiveLoginResponse::class.java ) - Result.success(data) } suspend fun testConnection(auth: InternetArchive.Auth): Result = - SaveClient.get(context).enqueueResult( + client.enqueue( Request.Builder() .url(ARCHIVE_API_ENDPOINT) .method("GET", null) .addHeader("Authorization", "LOW ${auth.access}:${auth.secret}") .build() - ) { response -> - Result.success(response.isSuccessful) + ).map { response -> + response.isSuccessful } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt deleted file mode 100644 index 87c6aef3..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt +++ /dev/null @@ -1,83 +0,0 @@ -package net.opendasharchive.openarchive.features.internetarchive.presentation - -import android.content.Intent -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme -import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.core.dialog.DialogHost -import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ComposeAppBar -import net.opendasharchive.openarchive.features.main.MainActivity -import org.koin.androidx.viewmodel.ext.android.viewModel - -@Deprecated("use jetpack compose") -class InternetArchiveActivity : AppCompatActivity() { - - val dialogManager: DialogStateManager by viewModel() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val (space, isNewSpace) = intent.extras.getSpace(Space.Type.INTERNET_ARCHIVE) - - setContent { - - SaveAppTheme { - - DialogHost(dialogManager) - - Scaffold( - topBar = { - ComposeAppBar( - title = stringResource(R.string.internet_archive), - onNavigationAction = { onComplete(IAResult.Cancelled) } - ) - } - ) { paddingValues -> - Box(modifier = Modifier - .fillMaxSize() - .padding(paddingValues)) { - InternetArchiveScreen(space, isNewSpace) { - onComplete(it) - } - } - } - } - - - } - } - - private fun onComplete(result: IAResult) { - when (result) { - IAResult.Saved -> { - startActivity(Intent(this, MainActivity::class.java)) - // measureNewBackend(Space.Type.INTERNET_ARCHIVE) - } - - IAResult.Deleted -> Space.navigate(this) - IAResult.Cancelled -> onBackPressed() - } - } -} - -//fun Activity.measureNewBackend(type: Space.Type) { -// CleanInsightsManager.getConsent(this) { -// CleanInsightsManager.measureEvent( -// "backend", -// "new", -// type.friendlyName -// ) -// } -//} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt deleted file mode 100644 index c2a2fb53..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt +++ /dev/null @@ -1,79 +0,0 @@ -package net.opendasharchive.openarchive.features.internetarchive.presentation - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.core.os.bundleOf -import androidx.fragment.app.setFragmentResult -import androidx.navigation.fragment.findNavController -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithNewSpace -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithSpaceId -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace -import net.opendasharchive.openarchive.features.core.BaseFragment -import net.opendasharchive.openarchive.features.core.ToolbarConfigurable - -@Deprecated("only used for backward compatibility") -class InternetArchiveFragment : BaseFragment(), ToolbarConfigurable { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - - val (space, isNewSpace) = arguments.getSpace(Space.Type.INTERNET_ARCHIVE) - - return ComposeView(requireContext()).apply { - setContent { - InternetArchiveScreen(space, isNewSpace) { result -> - finish(result) - } - } - } - } - - private fun finish(result: IAResult) { - if (isJetpackNavigation) { - when (result) { - IAResult.Saved -> { - val message = getString(R.string.you_have_successfully_connected_to_the_internet_archive) - val action = InternetArchiveFragmentDirections.actionFragmentInternetArchiveToFragmentSpaceSetupSuccess(message) - findNavController().navigate(action) - } - IAResult.Deleted -> TODO() - IAResult.Cancelled -> findNavController().popBackStack() - } - } else { - setFragmentResult(result.value, bundleOf()) - - if (result == IAResult.Saved) { - // activity?.measureNewBackend(Space.Type.INTERNET_ARCHIVE) - } - } - } - - companion object { - - val RESP_SAVED = IAResult.Saved.value - val RESP_CANCEL = IAResult.Cancelled.value - - @JvmStatic - fun newInstance(args: Bundle) = InternetArchiveFragment().apply { - arguments = args - } - - @JvmStatic - fun newInstance(spaceId: Long) = newInstance(args = bundleWithSpaceId(spaceId)) - - @JvmStatic - fun newInstance() = newInstance(args = bundleWithNewSpace()) - } - - override fun getToolbarTitle() = getString(R.string.internet_archive) - override fun shouldShowBackButton() = true -} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt deleted file mode 100644 index 4429b4cd..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.opendasharchive.openarchive.features.internetarchive.presentation - -import androidx.compose.runtime.Composable -import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme -import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult -import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsScreen -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen - -@Composable -fun InternetArchiveScreen(space: Space, isNewSpace: Boolean, onFinish: (IAResult) -> Unit) = SaveAppTheme { - if (isNewSpace) { - InternetArchiveLoginScreen(space) { - onFinish(it) - } - } else { - InternetArchiveDetailsScreen(space) { - onFinish(it) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsFragment.kt new file mode 100644 index 00000000..fe71c7b2 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsFragment.kt @@ -0,0 +1,51 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.details + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.compose.content +import androidx.navigation.fragment.findNavController +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.core.BaseFragment +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace + +class InternetArchiveDetailsFragment: BaseFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = content { + + val (space, isNewSpace) = arguments.getSpace(Space.Type.INTERNET_ARCHIVE) + + SaveAppTheme { + InternetArchiveDetailsScreen( + space = space, + dialogManager = dialogManager, + onResult = { result -> + handleResult(result) + } + ) + } + + } + + private fun handleResult(result: IAResult) { + when (result) { + IAResult.Saved -> TODO() + IAResult.Deleted -> { + findNavController().popBackStack() + } + IAResult.Cancelled -> { + findNavController().popBackStack() + } + } + } + + override fun getToolbarTitle(): String = getString(R.string.internet_archive) +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt index b006feb4..8cd15723 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt @@ -32,13 +32,18 @@ import net.opendasharchive.openarchive.features.core.UiText import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager import net.opendasharchive.openarchive.features.core.dialog.showDialog import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.CustomTextField import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @Composable -fun InternetArchiveDetailsScreen(space: Space, onResult: (IAResult) -> Unit) { +fun InternetArchiveDetailsScreen( + space: Space, + onResult: (IAResult) -> Unit, + dialogManager: DialogStateManager, +) { val viewModel: InternetArchiveDetailsViewModel = koinViewModel { parametersOf(space) } @@ -55,14 +60,36 @@ fun InternetArchiveDetailsScreen(space: Space, onResult: (IAResult) -> Unit) { } } - InternetArchiveDetailsContent(state, viewModel::dispatch) + InternetArchiveDetailsContent( + state = state, + onRemove = { + dialogManager.showDialog(dialogManager.requireResourceProvider()) { + title = UiText.StringResource(R.string.remove_from_app) + message = + UiText.StringResource(R.string.are_you_sure_you_want_to_remove_this_server_from_the_app) + icon = UiImage.DrawableResource(R.drawable.ic_trash) + destructiveButton { + text = UiText.StringResource(R.string.remove) + action = { + viewModel.dispatch(Action.Remove) + } + } + + neutralButton { + text = UiText.StringResource(R.string.action_cancel) + action = { + //dismiss + } + } + } + } + ) } @Composable private fun InternetArchiveDetailsContent( state: InternetArchiveDetailsState, - dispatch: Dispatch, - dialogManager: DialogStateManager = koinViewModel() + onRemove: () -> Unit, ) { Box( @@ -73,7 +100,7 @@ private fun InternetArchiveDetailsContent( Column { - //InternetArchiveHeader() + Text(stringResource(R.string.account), fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface) Spacer(Modifier.height(ThemeDimensions.spacing.large)) @@ -110,28 +137,7 @@ private fun InternetArchiveDetailsContent( horizontalArrangement = Arrangement.Center ) { TextButton( - onClick = { - //isRemoving = true - - dialogManager.showDialog(dialogManager.requireResourceProvider()) { - title = UiText.StringResource(R.string.remove_from_app) - message = UiText.StringResource(R.string.are_you_sure_you_want_to_remove_this_server_from_the_app) - icon = UiImage.DrawableResource(R.drawable.ic_trash) - destructiveButton { - text = UiText.StringResource(R.string.remove) - action = { - dispatch(Action.Remove) - } - } - - neutralButton { - text = UiText.StringResource(R.string.action_cancel) - action = { - //dismiss - } - } - } - }, + onClick = onRemove, colors = ButtonDefaults.textButtonColors( contentColor = MaterialTheme.colorScheme.error ) @@ -161,7 +167,7 @@ private fun InternetArchiveScreenPreview() { userName = "@abc_name", screenName = "ABC Name" ), - dispatch = {} + onRemove = {} ) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginFragment.kt new file mode 100644 index 00000000..00794f8c --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginFragment.kt @@ -0,0 +1,53 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.login + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.compose.content +import androidx.navigation.fragment.findNavController +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.core.BaseFragment +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace + +class InternetArchiveLoginFragment: BaseFragment() { + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = content { + + val (space, isNewSpace) = arguments.getSpace(Space.Type.INTERNET_ARCHIVE) + SaveAppTheme { + InternetArchiveLoginScreen(space) { result -> + handleResult(result) + } + } + } + + private fun handleResult(result: IAResult) { + when (result) { + IAResult.Saved -> { + val message = getString(R.string.you_have_successfully_connected_to_the_internet_archive) + val action = + InternetArchiveLoginFragmentDirections.actionFragmentInternetArchiveLoginToFragmentSpaceSetupSuccess( + message = message, + ) + findNavController().navigate(action) + } + IAResult.Deleted -> { + findNavController().popBackStack() + } + IAResult.Cancelled -> { + findNavController().popBackStack() + } + } + } + + override fun getToolbarTitle(): String = getString(R.string.internet_archive) +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index 12bdc378..dcf2872f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,5 +1,6 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Immutable import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt index 13215329..806e4bfa 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt @@ -47,6 +47,7 @@ import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.extensions.getMeasurments import net.opendasharchive.openarchive.features.core.BaseActivity +import net.opendasharchive.openarchive.features.core.NavArgument import net.opendasharchive.openarchive.features.core.UiImage import net.opendasharchive.openarchive.features.core.UiText import net.opendasharchive.openarchive.features.core.asUiImage @@ -56,7 +57,6 @@ import net.opendasharchive.openarchive.features.core.dialog.DialogConfig import net.opendasharchive.openarchive.features.core.dialog.DialogType import net.opendasharchive.openarchive.features.core.dialog.showDialog import net.opendasharchive.openarchive.features.core.dialog.showInfoDialog -import net.opendasharchive.openarchive.features.folders.AddFolderActivity import net.opendasharchive.openarchive.features.main.adapters.FolderDrawerAdapter import net.opendasharchive.openarchive.features.main.adapters.FolderDrawerAdapterListener import net.opendasharchive.openarchive.features.main.adapters.SpaceDrawerAdapter @@ -134,7 +134,7 @@ class MainActivity : BaseActivity(), SpaceDrawerAdapterListener, FolderDrawerAda private val mNewFolderResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK) { - refreshProjects(it.data?.getLongExtra(AddFolderActivity.EXTRA_FOLDER_ID, -1)) + refreshProjects(it.data?.getLongExtra(NavArgument.FOLDER_ID, -1)) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaScreen.kt index 64a64207..65cfaebb 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ui/MainMediaScreen.kt @@ -7,9 +7,7 @@ import android.os.Handler import android.os.Looper import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -22,9 +20,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error @@ -56,8 +51,6 @@ import net.opendasharchive.openarchive.db.Collection import net.opendasharchive.openarchive.db.Media import net.opendasharchive.openarchive.features.media.PreviewActivity import net.opendasharchive.openarchive.upload.BroadcastManager -import net.opendasharchive.openarchive.upload.UploadManagerActivity -import org.koin.androidx.compose.koinViewModel /** * A data class representing one “section” (i.e. one Collection and its list of Media). @@ -400,7 +393,7 @@ private fun handleMediaClick(context: Context, media: Media, onError: (Media) -> Media.Status.Queued, Media.Status.Uploading -> { // Start the upload manager activity - context.startActivity(Intent(context, UploadManagerActivity::class.java)) + //TODO: show the bottom sheet for EditUploads here } Media.Status.Error -> { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/ReviewActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/ReviewActivity.kt index d04b1dd7..1bc50712 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/media/ReviewActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/ReviewActivity.kt @@ -2,6 +2,7 @@ package net.opendasharchive.openarchive.features.media import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -25,6 +26,7 @@ import net.opendasharchive.openarchive.util.extensions.hide import net.opendasharchive.openarchive.util.extensions.show import net.opendasharchive.openarchive.util.extensions.toggle import java.text.NumberFormat +import kotlin.arrayOf import kotlin.math.max import kotlin.math.min @@ -60,6 +62,12 @@ class ReviewActivity : BaseActivity(), View.OnClickListener { private val mMedia get() = mStore.getOrNull(mIndex) + private val mPicasso by lazy { + Picasso.Builder(this) + .addRequestHandler(VideoRequestHandler(this)) + .build() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -323,9 +331,7 @@ class ReviewActivity : BaseActivity(), View.OnClickListener { .into(imageView) } else if (media?.mimeType?.startsWith("video") == true) { - Picasso.Builder(this) - .addRequestHandler(VideoRequestHandler(this)) - .build() + mPicasso .load(VideoRequestHandler.SCHEME_VIDEO + ":" + media.originalFilePath) ?.fit() ?.centerCrop() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt index 144d87a7..c4e732cd 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt @@ -1,6 +1,9 @@ package net.opendasharchive.openarchive.features.onboarding +import android.content.Intent import android.os.Bundle +import androidx.annotation.IdRes +import androidx.core.bundle.bundleOf import androidx.fragment.app.Fragment import androidx.navigation.NavController import androidx.navigation.NavGraph @@ -11,12 +14,14 @@ import androidx.navigation.ui.setupActionBarWithNavController import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.databinding.ActivitySpaceSetupBinding import net.opendasharchive.openarchive.features.core.BaseActivity +import net.opendasharchive.openarchive.features.core.NavArgument import net.opendasharchive.openarchive.features.core.ToolbarConfigurable enum class StartDestination { SPACE_TYPE, SPACE_LIST, DWEB_DASHBOARD, + FOLDER_LIST, ADD_FOLDER, ADD_NEW_FOLDER } @@ -60,31 +65,39 @@ class SpaceSetupActivity : BaseActivity() { initSpaceSetupNavigation() } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + navController.handleDeepLink(intent) + } + private fun initSpaceSetupNavigation() { - val navHostFragment = + val navHost = supportFragmentManager.findFragmentById(R.id.space_nav_host_fragment) as NavHostFragment - navController = navHostFragment.navController + navController = navHost.navController navGraph = navController.navInflater.inflate(R.navigation.space_setup_navigation) - val startDestinationString = - intent.getStringExtra("start_destination") ?: StartDestination.SPACE_TYPE.name - val startDestination = StartDestination.valueOf(startDestinationString) - when (startDestination) { - StartDestination.SPACE_LIST -> { - navGraph.setStartDestination(R.id.fragment_space_list) - } - StartDestination.ADD_FOLDER -> { - navGraph.setStartDestination(R.id.fragment_add_folder) - } - StartDestination.ADD_NEW_FOLDER -> { - navGraph.setStartDestination(R.id.fragment_create_new_folder) - } - else -> { - navGraph.setStartDestination(R.id.fragment_space_setup) - } + val startDestName = + intent.getStringExtra(NavArgument.START_DESTINATION) ?: StartDestination.SPACE_TYPE.name + val startDestination = StartDestination.valueOf(startDestName) + @IdRes val startDest = when (startDestination) { + StartDestination.SPACE_TYPE -> R.id.fragment_space_setup + StartDestination.SPACE_LIST -> R.id.fragment_space_list + StartDestination.DWEB_DASHBOARD -> R.id.fragment_snowbird + StartDestination.FOLDER_LIST -> R.id.fragment_folder_list + StartDestination.ADD_FOLDER -> R.id.fragment_add_folder + StartDestination.ADD_NEW_FOLDER -> R.id.fragment_create_new_folder } - navController.graph = navGraph + + val startArgs: Bundle? = if (startDestination == StartDestination.FOLDER_LIST) { + bundleOf(NavArgument.SHOW_ARCHIVED_FOLDERS to intent.getBooleanExtra(NavArgument.SHOW_ARCHIVED_FOLDERS, false)) + bundleOf(NavArgument.SPACE_ID to intent.getLongExtra(NavArgument.SPACE_ID, -1L)) + bundleOf(NavArgument.FOLDER_ID to intent.getLongExtra(NavArgument.FOLDER_ID, -1L)) + } else null + + navGraph.setStartDestination(startDest) + + navController.setGraph(navGraph, startArgs) appBarConfiguration = AppBarConfiguration(emptySet()) setupActionBarWithNavController(navController, appBarConfiguration) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt deleted file mode 100644 index 9f0ef994..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderActivity.kt +++ /dev/null @@ -1,134 +0,0 @@ -package net.opendasharchive.openarchive.features.settings - -import android.os.Bundle -import android.view.MenuItem -import android.view.inputmethod.EditorInfo -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.databinding.ActivityEditFolderBinding -import net.opendasharchive.openarchive.db.Project -import net.opendasharchive.openarchive.features.core.BaseActivity -import net.opendasharchive.openarchive.features.core.UiImage -import net.opendasharchive.openarchive.features.core.UiText -import net.opendasharchive.openarchive.features.core.dialog.DialogType -import net.opendasharchive.openarchive.features.core.dialog.showDialog -import net.opendasharchive.openarchive.util.extensions.Position -import net.opendasharchive.openarchive.util.extensions.setDrawable - -class EditFolderActivity : BaseActivity() { - - companion object { - const val EXTRA_CURRENT_PROJECT_ID = "archive_extra_current_project_id" - } - - private lateinit var mProject: Project - private lateinit var mBinding: ActivityEditFolderBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val project = Project.getById(intent.getLongExtra(EXTRA_CURRENT_PROJECT_ID, -1L)) - ?: return finish() - - mProject = project - - mBinding = ActivityEditFolderBinding.inflate(layoutInflater) - setContentView(mBinding.root) - - - setupToolbar("Edit Folder") - - mBinding.folderName.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - val newName = mBinding.folderName.text.toString() - - if (newName.isNotBlank()) { - mProject.description = newName - mProject.save() - - supportActionBar?.title = newName - mBinding.folderName.hint = newName - - - setupToolbar(newName) - } - } - - false - } - - mBinding.btRemove.setOnClickListener { - showDeleteFolderConfirmDialog() - } - - mBinding.btArchive.setOnClickListener { - archiveProject() - } - - CreativeCommonsLicenseManager.initialize(mBinding.cc, null) { - mProject.licenseUrl = it - mProject.save() - } - - updateUi() - } - - private fun showDeleteFolderConfirmDialog() { - dialogManager.showDialog(dialogManager.requireResourceProvider()) { - type = DialogType.Error - icon = UiImage.DrawableResource(R.drawable.ic_trash) - title = UiText.StringResource(R.string.remove_from_app) - message = UiText.StringResource(R.string.action_remove_project) - destructiveButton { - text = UiText.StringResource(R.string.remove) - action = { - mProject.delete() - finish() - } - } - neutralButton { - text = UiText.StringResource(R.string.lbl_Cancel) - action = { - dialogManager.dismissDialog() - } - } - } - } - - private fun archiveProject() { - mProject.isArchived = !mProject.isArchived - mProject.save() - - updateUi() - } - - private fun updateUi() { - supportActionBar?.title = mProject.description - - mBinding.folderName.isEnabled = !mProject.isArchived - mBinding.folderName.hint = mProject.description - mBinding.folderName.setText(mProject.description) - - mBinding.btArchive.setText(if (mProject.isArchived) - R.string.action_unarchive_project else - R.string.action_archive_project) - - val global = mProject.space?.license != null - - if (global) { - mBinding.cc.tvCcLabel.setText(R.string.set_the_same_creative_commons_license_for_all_folders_on_this_server) - } - - CreativeCommonsLicenseManager.initialize(mBinding.cc, mProject.licenseUrl, !mProject.isArchived && !global) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - finish() - return true - } - } - - return super.onOptionsItemSelected(item) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderFragment.kt new file mode 100644 index 00000000..16cfb915 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/EditFolderFragment.kt @@ -0,0 +1,135 @@ +package net.opendasharchive.openarchive.features.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import androidx.navigation.fragment.findNavController +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.databinding.FragmentEditFolderBinding +import net.opendasharchive.openarchive.db.Project +import net.opendasharchive.openarchive.features.core.BaseFragment +import net.opendasharchive.openarchive.features.core.NavArgument +import net.opendasharchive.openarchive.features.core.UiImage +import net.opendasharchive.openarchive.features.core.UiText +import net.opendasharchive.openarchive.features.core.dialog.DialogType +import net.opendasharchive.openarchive.features.core.dialog.showDialog + +class EditFolderFragment : BaseFragment() { + + private lateinit var project: Project + private lateinit var binding: FragmentEditFolderBinding + + override fun getToolbarTitle(): String = "Edit Folder" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + arguments?.let { + val folderId = it.getLong(NavArgument.FOLDER_ID) + if (folderId == -1L) { + throw IllegalArgumentException("Folder ID cannot be -1") + } + project = Project.getById(folderId) ?: + throw IllegalArgumentException("Project not found for ID: $folderId") + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentEditFolderBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.folderName.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + val newName = binding.folderName.text.toString() + + if (newName.isNotBlank()) { + project.description = newName + project.save() + + //TODO: update toolbar title + //supportActionBar?.title = newName + binding.folderName.hint = newName + + + //setupToolbar(newName) + } + } + + false + } + + binding.btRemove.setOnClickListener { + showDeleteFolderConfirmDialog() + } + + binding.btArchive.setOnClickListener { + archiveProject() + } + + CreativeCommonsLicenseManager.initialize(binding.cc, null) { + project.licenseUrl = it + project.save() + } + + updateUi() + } + + private fun showDeleteFolderConfirmDialog() { + dialogManager.showDialog(dialogManager.requireResourceProvider()) { + type = DialogType.Error + icon = UiImage.DrawableResource(R.drawable.ic_trash) + title = UiText.StringResource(R.string.remove_from_app) + message = UiText.StringResource(R.string.action_remove_project) + destructiveButton { + text = UiText.StringResource(R.string.remove) + action = { + project.delete() + findNavController().popBackStack() + } + } + neutralButton { + text = UiText.StringResource(R.string.lbl_Cancel) + action = { + dialogManager.dismissDialog() + } + } + } + } + + private fun archiveProject() { + project.isArchived = !project.isArchived + project.save() + + updateUi() + } + + private fun updateUi() { + //supportActionBar?.title = project.description + + binding.folderName.isEnabled = !project.isArchived + binding.folderName.hint = project.description + binding.folderName.setText(project.description) + + binding.btArchive.setText(if (project.isArchived) + R.string.action_unarchive_project else + R.string.action_archive_project) + + val global = project.space?.license != null + + if (global) { + binding.cc.tvCcLabel.setText(R.string.set_the_same_creative_commons_license_for_all_folders_on_this_server) + } + + CreativeCommonsLicenseManager.initialize(binding.cc, project.licenseUrl, !project.isArchived && !global) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/FolderListFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/FolderListFragment.kt new file mode 100644 index 00000000..3f936571 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/FolderListFragment.kt @@ -0,0 +1,91 @@ +package net.opendasharchive.openarchive.features.settings + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import net.opendasharchive.openarchive.FolderAdapter +import net.opendasharchive.openarchive.FolderAdapterListener +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.databinding.FragmentFolderListBinding +import net.opendasharchive.openarchive.db.Project +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.core.BaseFragment +import net.opendasharchive.openarchive.features.core.NavArgument +import net.opendasharchive.openarchive.util.extensions.toggle + +class FolderListFragment : BaseFragment(), FolderAdapterListener { + + private lateinit var binding: FragmentFolderListBinding + private lateinit var adapter: FolderAdapter + + private var showArchived = true + private var selectedSpaceId = -1L + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + arguments?.let { + showArchived = it.getBoolean(NavArgument.SHOW_ARCHIVED_FOLDERS, false) + selectedSpaceId = it.getLong(NavArgument.SPACE_ID, -1L) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentFolderListBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupRecyclerView() + } + + private fun setupRecyclerView() { + adapter = FolderAdapter(context = requireContext(), listener = this, isArchived = showArchived) + binding.rvProjects.layoutManager = LinearLayoutManager(requireContext()) + binding.rvProjects.adapter = adapter + } + + override fun onResume() { + super.onResume() + refreshProjects() + } + + private fun refreshProjects() { + val projects = if (showArchived) { + Space.current?.archivedProjects + } else { + Space.current?.projects?.filter { !it.isArchived } + } ?: emptyList() + + adapter.update(projects) + + if (projects.isEmpty()) { + binding.rvProjects.visibility = View.GONE + binding.tvNoFolders.visibility = View.VISIBLE + } else { + binding.rvProjects.visibility = View.VISIBLE + binding.tvNoFolders.visibility = View.GONE + } + } + + + override fun projectClicked(project: Project) { + val action = FolderListFragmentDirections.actionFragmentFolderListToFragmentEditFolder(folderId = project.id) + findNavController().navigate(action) + } + + override fun getToolbarTitle(): String = getString(R.string.archived_folders) +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt deleted file mode 100644 index 3e1ac954..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/FoldersActivity.kt +++ /dev/null @@ -1,144 +0,0 @@ -package net.opendasharchive.openarchive.features.settings - -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager -import net.opendasharchive.openarchive.FolderAdapter -import net.opendasharchive.openarchive.FolderAdapterListener -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.databinding.ActivityFoldersBinding -import net.opendasharchive.openarchive.db.Project -import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.core.BaseActivity -import net.opendasharchive.openarchive.util.extensions.toggle - -class FoldersActivity : BaseActivity(), FolderAdapterListener { - - companion object { - const val EXTRA_SHOW_ARCHIVED = "show_archived" - const val EXTRA_SELECTED_SPACE_ID = "selected_space_id" - const val EXTRA_SELECTED_PROJECT_ID = "SELECTED_PROJECT_ID" - } - - private lateinit var mBinding: ActivityFoldersBinding - private lateinit var mAdapter: FolderAdapter - - private var mArchived = true - private var mSelectedSpaceId = -1L - private var mSelectedProjectId: Long = -1L - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - mArchived = intent.getBooleanExtra(EXTRA_SHOW_ARCHIVED, false) - mSelectedSpaceId = intent.getLongExtra(EXTRA_SELECTED_SPACE_ID, -1L) - mSelectedProjectId = intent.getLongExtra(EXTRA_SELECTED_PROJECT_ID, -1L) - - mBinding = ActivityFoldersBinding.inflate(layoutInflater) - setContentView(mBinding.root) - - setupToolbar( - title = getString(if (mArchived) R.string.archived_folders else R.string.folders), - showBackButton = true - ) - - setupRecyclerView() - - setupButtons() - } - - private fun setupRecyclerView() { - mAdapter = FolderAdapter(context = this, listener = this, isArchived = mArchived) - mBinding.rvProjects.layoutManager = LinearLayoutManager(this) - mBinding.rvProjects.adapter = mAdapter - } - - private fun setupButtons() { -// mBinding.fabAdd.apply { -// visibility = if (mArchived) View.INVISIBLE else View.VISIBLE -// setOnClickListener { addFolder() } -// } - - mBinding.btViewArchived.apply { - toggle(!mArchived) - setOnClickListener { - val i = Intent(this@FoldersActivity, FoldersActivity::class.java) - i.putExtra(EXTRA_SHOW_ARCHIVED, true) - startActivity(i) - } - } - } - - override fun onResume() { - super.onResume() - refreshProjects() - invalidateOptionsMenu() - } - - private fun refreshProjects() { - val projects = if (mArchived) { - Space.current?.archivedProjects - } else { - Space.current?.projects?.filter { !it.isArchived } - } ?: emptyList() - - mAdapter.update(projects) - - if (projects.isEmpty()) { - mBinding.rvProjects.visibility = View.GONE - mBinding.tvNoFolders.visibility = View.VISIBLE - } else { - mBinding.rvProjects.visibility = View.VISIBLE - mBinding.tvNoFolders.visibility = View.GONE - } - } - - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.menu_folder_list, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onPrepareOptionsMenu(menu: Menu?): Boolean { - val archivedCount = Space.get(mSelectedSpaceId)?.archivedProjects?.size ?: 0 - menu?.findItem(R.id.action_archived_folders)?.isVisible = (!mArchived && archivedCount > 0) - return super.onPrepareOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - - R.id.action_archived_folders -> { - navigateToArchivedFolders() - true - } - - else -> super.onOptionsItemSelected(item) - } - } - - private fun navigateToArchivedFolders() { - val intent = Intent(this, FoldersActivity::class.java).apply { - putExtra(EXTRA_SHOW_ARCHIVED, true) - putExtra(EXTRA_SELECTED_SPACE_ID, mSelectedSpaceId) - putExtra(EXTRA_SELECTED_PROJECT_ID, mSelectedProjectId) - } - startActivity(intent) - } - - - override fun projectClicked(project: Project) { - val resultIntent = Intent() - resultIntent.putExtra("SELECTED_FOLDER_ID", project.id) - setResult(RESULT_OK, resultIntent) - //finish() // Close FoldersActivity and return to MainActivity - - val intent = Intent(this, EditFolderActivity::class.java).apply { - putExtra(EditFolderActivity.EXTRA_CURRENT_PROJECT_ID, project.id) - } - startActivity(intent) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/GeneralSettingsActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/GeneralSettingsActivity.kt deleted file mode 100644 index 82fdee99..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/GeneralSettingsActivity.kt +++ /dev/null @@ -1,178 +0,0 @@ -package net.opendasharchive.openarchive.features.settings - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.MenuItem -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog -import androidx.compose.ui.res.stringResource -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.SwitchPreferenceCompat -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.databinding.ActivitySettingsContainerBinding -import net.opendasharchive.openarchive.features.core.BaseActivity -import net.opendasharchive.openarchive.features.settings.passcode.PasscodeRepository -import net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupActivity -import net.opendasharchive.openarchive.util.Prefs -import net.opendasharchive.openarchive.util.Theme -import org.koin.android.ext.android.inject - - -class GeneralSettingsActivity: BaseActivity() { - - class Fragment: PreferenceFragmentCompat() { - - private val passcodeRepository by inject() - -// private var mCiConsentPref: SwitchPreferenceCompat? = null - - private var passcodePreference: SwitchPreferenceCompat? = null - - private val activityResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - val passcodeEnabled = result.data?.getBooleanExtra("passcode_enabled", false) ?: false - passcodePreference?.isChecked = passcodeEnabled - } else { - passcodePreference?.isChecked = false - } - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.prefs_general, rootKey) - - passcodePreference = findPreference(Prefs.PASSCODE_ENABLED) - -// findPreference(Prefs.PASSCODE_ENABLED)?.setOnPreferenceChangeListener { _, newValue -> -// //Prefs.lockWithPasscode = newValue as Boolean -// if (newValue as? Boolean == true) { -// -// val intent = Intent(context, PasscodeSetupActivity::class.java) -// activityResultLauncher.launch(intent) -// } -// false -// } - - - passcodePreference?.setOnPreferenceChangeListener { _, newValue -> - val enabled = newValue as Boolean - if (enabled) { - // Launch PasscodeSetupActivity - val intent = Intent(context, PasscodeSetupActivity::class.java) - activityResultLauncher.launch(intent) - } else { - // Show confirmation dialog - AlertDialog.Builder(requireContext()) - .setTitle("Disable Passcode") - .setMessage("Are you sure you want to disable the passcode?") - .setPositiveButton("Yes") { _, _ -> - passcodeRepository.clearPasscode() - passcodePreference?.isChecked = false - - // Update the FLAG_SECURE dynamically - (activity as? BaseActivity)?.updateScreenshotPrevention() - } - .setNegativeButton("No") { _, _ -> - passcodePreference?.isChecked = true - } - .show() - } - // Return false to avoid the preference updating immediately - false - } - -// findPreference(Prefs.USE_TOR)?.setOnPreferenceChangeListener { _, newValue -> -// val activity = activity ?: return@setOnPreferenceChangeListener true -// -// if (newValue as Boolean) { -// if (!OrbotHelper.isOrbotInstalled(activity) && !OrbotHelper.isTorServicesInstalled(activity)) { -// AlertHelper.show(activity, -// R.string.prefs_install_tor_summary, -// R.string.prefs_use_tor_title, -// buttons = listOf( -// AlertHelper.positiveButton(R.string.action_install) { _, _ -> -// activity.startActivity( -// OrbotHelper.getOrbotInstallIntent(activity)) -// }, -// AlertHelper.negativeButton(R.string.action_cancel) -// )) -// -// return@setOnPreferenceChangeListener false -// } -// } -// -// true -// } - - findPreference("proof_mode")?.setOnPreferenceClickListener { - startActivity(Intent(context, ProofModeSettingsActivity::class.java)) - - true - } - - findPreference(Prefs.THEME)?.setOnPreferenceChangeListener { _, newValue -> - Theme.set(requireActivity(), Theme.get(newValue as? String)) - - true - } - - findPreference(Prefs.PROHIBIT_SCREENSHOTS)?.setOnPreferenceClickListener { _ -> - if (activity is BaseActivity) { - // make sure this gets settings change gets applied instantly - // (all other activities rely on the hook in BaseActivity.onResume()) - (activity as BaseActivity).updateScreenshotPrevention() - } - - true - } - -// mCiConsentPref = findPreference("health_checks") -// -// mCiConsentPref?.setOnPreferenceChangeListener { _, newValue -> -// if (newValue as? Boolean == false) { -// CleanInsightsManager.deny() -// } -// else { -// startActivity(Intent(context, ConsentActivity::class.java)) -// } -// -// true -// } - } - -// override fun onResume() { -// super.onResume() -// -// mCiConsentPref?.isChecked = CleanInsightsManager.hasConsent() -// } - } - - - private lateinit var mBinding: ActivitySettingsContainerBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - mBinding = ActivitySettingsContainerBinding.inflate(layoutInflater) - setContentView(mBinding.root) - - setupToolbar(title = getString(R.string.general)) - - supportFragmentManager - .beginTransaction() - .replace(mBinding.container.id, Fragment()) - .commit() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - finish() - return true - } - - return super.onOptionsItemSelected(item) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/OpenOrbotPreference.java b/app/src/main/java/net/opendasharchive/openarchive/features/settings/OpenOrbotPreference.java new file mode 100644 index 00000000..a8849a28 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/OpenOrbotPreference.java @@ -0,0 +1,38 @@ +package net.opendasharchive.openarchive.features.settings; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import net.opendasharchive.openarchive.R; + +public class OpenOrbotPreference extends Preference { + public OpenOrbotPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setWidgetLayoutResource(R.layout.prefs_open_orbot); + this.setSelectable(false); + this.setOnPreferenceClickListener(null); + } + + private View.OnClickListener onClickListener = null; + + public void setOnOpenOrbotListener(View.OnClickListener listener) { + this.onClickListener = listener; + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + Button button = (Button) holder.findViewById(R.id.open_orbot_button); + button.setOnClickListener(v -> { + if (onClickListener != null) { + onClickListener.onClick(v); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeSettingsActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeSettingsActivity.kt index 7f8624e2..f9d5077b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeSettingsActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/ProofModeSettingsActivity.kt @@ -23,7 +23,7 @@ import com.permissionx.guolindev.PermissionX import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.databinding.ActivitySettingsContainerBinding +import net.opendasharchive.openarchive.databinding.ActivityProofmodeSettingsBinding import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.util.Hbks import net.opendasharchive.openarchive.util.Prefs @@ -167,12 +167,12 @@ class ProofModeSettingsActivity : BaseActivity() { } } - private lateinit var mBinding: ActivitySettingsContainerBinding + private lateinit var mBinding: ActivityProofmodeSettingsBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mBinding = ActivitySettingsContainerBinding.inflate(layoutInflater) + mBinding = ActivityProofmodeSettingsBinding.inflate(layoutInflater) setContentView(mBinding.root) setupToolbar(getString(R.string.proofmode)) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt index 4965a2f3..4d866716 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt @@ -5,11 +5,17 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts +import androidx.navigation.NavDeepLinkBuilder +import androidx.lifecycle.lifecycleScope +import androidx.preference.EditTextPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat +import kotlinx.coroutines.launch import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity +import net.opendasharchive.openarchive.features.core.NavArgument import net.opendasharchive.openarchive.features.core.UiText import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager import net.opendasharchive.openarchive.features.core.dialog.DialogType @@ -18,11 +24,14 @@ import net.opendasharchive.openarchive.features.onboarding.SpaceSetupActivity import net.opendasharchive.openarchive.features.onboarding.StartDestination import net.opendasharchive.openarchive.features.settings.passcode.PasscodeRepository import net.opendasharchive.openarchive.features.settings.passcode.passcode_setup.PasscodeSetupActivity +import net.opendasharchive.openarchive.services.tor.TorStatus +import net.opendasharchive.openarchive.services.tor.TorViewModel import net.opendasharchive.openarchive.util.Prefs import net.opendasharchive.openarchive.util.Theme import net.opendasharchive.openarchive.util.extensions.getVersionName import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.activityViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel class SettingsFragment : PreferenceFragmentCompat() { @@ -30,9 +39,10 @@ class SettingsFragment : PreferenceFragmentCompat() { private val dialogManager: DialogStateManager by activityViewModel() - private var passcodePreference: SwitchPreferenceCompat? = null + private val torViewModel: TorViewModel by viewModel() + private val activityResultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> @@ -67,7 +77,6 @@ class SettingsFragment : PreferenceFragmentCompat() { ) { setPreferencesFromResource(R.xml.prefs_general, rootKey) - passcodePreference = findPreference(Prefs.PASSCODE_ENABLED) passcodePreference?.setOnPreferenceChangeListener { _, newValue -> @@ -115,15 +124,34 @@ class SettingsFragment : PreferenceFragmentCompat() { getPrefByKey(R.string.pref_media_servers)?.setOnPreferenceClickListener { val intent = Intent(context, SpaceSetupActivity::class.java) - intent.putExtra("start_destination", StartDestination.SPACE_LIST.name) + intent.putExtra(NavArgument.START_DESTINATION, StartDestination.SPACE_LIST.name) startActivity(intent) true } getPrefByKey(R.string.pref_media_folders)?.setOnPreferenceClickListener { - val intent = Intent(context, FoldersActivity::class.java) - intent.putExtra(FoldersActivity.EXTRA_SHOW_ARCHIVED, true) +// val intent = Intent(context, FoldersActivity::class.java) +// intent.putExtra(FoldersActivity.EXTRA_SHOW_ARCHIVED, true) +// startActivity(intent) +// + val intent = Intent(context, SpaceSetupActivity::class.java) + intent.putExtra(NavArgument.START_DESTINATION, StartDestination.FOLDER_LIST.name) + intent.putExtra(NavArgument.SHOW_ARCHIVED_FOLDERS, true) + intent.putExtra(NavArgument.SPACE_ID, Space.current?.id) startActivity(intent) + +// val args = Bundle().apply { +// putBoolean(NavArgument.SHOW_ARCHIVED_FOLDERS, true) +// putLong(NavArgument.SPACE_ID, -1L) +// putLong(NavArgument.FOLDER_ID, -1L) +// } +// NavDeepLinkBuilder(requireContext()) +// .setComponentName(SpaceSetupActivity::class.java) +// .setGraph(R.navigation.space_setup_navigation) +// .setDestination(R.id.fragment_folder_list) +// .setArguments(args) +// .createPendingIntent() +// .send() true } @@ -132,32 +160,57 @@ class SettingsFragment : PreferenceFragmentCompat() { true } - findPreference(Prefs.USE_TOR)?.setOnPreferenceChangeListener { _, newValue -> - Prefs.useTor = (newValue as Boolean) - //torViewModel.updateTorServiceState() - true - } - - getPrefByKey(R.string.pref_key_use_tor)?.apply { - isEnabled = true + val useTorPref = findPreference(Prefs.USE_TOR) + val torStatusPref = findPreference("tor_status") + val openOrbot = findPreference("open_orbot") - setOnPreferenceClickListener { - dialogManager.showDialog(dialogManager.requireResourceProvider()) { - type = DialogType.Info - title = UiText.StringResource(R.string.tor_disabled_title) - message = UiText.StringResource(R.string.tor_disabled_message) - positiveButton { - text = UiText.StringResource(android.R.string.ok) - } + val setUseTorText: (TorStatus, Boolean) -> Unit = { torStatus, enabled -> + if (torStatus == TorStatus.CONNECTED) { + if (enabled) { + torStatusPref?.setSummary(R.string.prefs_use_tor_enabled) + } else { + torStatusPref?.setSummary(R.string.prefs_use_tor_ready) + } + } else if (torStatus == TorStatus.CONNECTING) { + if (enabled) { + torStatusPref?.setSummary(R.string.prefs_use_tor_starting) + } else { + torStatusPref?.setSummary(R.string.prefs_use_tor_not_starting) + } + } else { + if (enabled) { + torStatusPref?.setSummary(R.string.prefs_use_tor_disabled) + } else { + torStatusPref?.setSummary(R.string.prefs_use_tor_not_ready) } - true } + } + lifecycleScope.launch { + torViewModel.torStatus.collect { torStatus -> + setUseTorText(torStatus, useTorPref?.isChecked == true) + } + } + useTorPref?.apply { + + setUseTorText(torViewModel.torStatus.value, isChecked) setOnPreferenceChangeListener { _, newValue -> - false + val enabled = newValue as Boolean + torViewModel.toggleTorServiceState(requireActivity(), enabled) + setUseTorText(torViewModel.torStatus.value, enabled) + torStatusPref?.isVisible = enabled + openOrbot?.isVisible = enabled + true } } + torStatusPref?.isVisible = useTorPref?.isChecked == true + openOrbot?.isVisible = useTorPref?.isChecked == true + + openOrbot?.setOnOpenOrbotListener { + torViewModel.requestOpenOrInstallOrbot(requireActivity()) + } + findPreference(Prefs.THEME)?.setOnPreferenceChangeListener { _, newValue -> Theme.set(requireActivity(), Theme.get(newValue as? String)) true @@ -202,4 +255,10 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) } + + override fun onResume() { + super.onResume() + //torViewModel.requestTorStatus() + } + } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt index 34c0448c..2b52ec42 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SpaceSetupFragment.kt @@ -33,42 +33,28 @@ class SpaceSetupFragment : BaseFragment() { // Prepare click lambdas that use the fragment’s business logic. val onWebDavClick = { - if (isJetpackNavigation) { - findNavController().navigate(R.id.action_fragment_space_setup_to_fragment_web_dav) - } else { - setFragmentResult( - RESULT_REQUEST_KEY, - bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_WEBDAV) - ) - } + + val action = SpaceSetupFragmentDirections.actionFragmentSpaceSetupToFragmentWebDav() + findNavController().navigate(action) + } // Only enable Internet Archive if not already present val isInternetArchiveAllowed = !Space.has(Space.Type.INTERNET_ARCHIVE) val onInternetArchiveClick = { - if (isJetpackNavigation) { + val action = - SpaceSetupFragmentDirections.actionFragmentSpaceSetupToFragmentInternetArchive() + SpaceSetupFragmentDirections.actionFragmentSpaceSetupToFragmentInternetArchiveLogin() findNavController().navigate(action) - } else { - setFragmentResult( - RESULT_REQUEST_KEY, - bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_INTERNET_ARCHIVE) - ) - } + } // Show/hide Snowbird based on config val isDwebEnabled = appConfig.isDwebEnabled val onDwebClicked = { - if (isJetpackNavigation) { + val action = SpaceSetupFragmentDirections.actionFragmentSpaceSetupToFragmentSnowbird() findNavController().navigate(action) - } else { - setFragmentResult( - RESULT_REQUEST_KEY, - bundleOf(RESULT_BUNDLE_KEY to RESULT_VAL_RAVEN) - ) - } + } SaveAppTheme { @@ -83,16 +69,6 @@ class SpaceSetupFragment : BaseFragment() { } - companion object { - const val RESULT_REQUEST_KEY = "space_setup_fragment_result" - const val RESULT_BUNDLE_KEY = "space_setup_result_key" - const val RESULT_VAL_DROPBOX = "dropbox" - const val RESULT_VAL_WEBDAV = "webdav" - const val RESULT_VAL_RAVEN = "raven" - const val RESULT_VAL_INTERNET_ARCHIVE = "internet_archive" - const val RESULT_VAL_GDRIVE = "gdrive" - } - override fun getToolbarTitle() = getString(R.string.space_setup_title) override fun getToolbarSubtitle(): String? = null override fun shouldShowBackButton() = true diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListFragment.kt index bbac2982..e4e91af3 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/spaces/SpaceListFragment.kt @@ -6,20 +6,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.navigation.fragment.findNavController import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme -import net.opendasharchive.openarchive.databinding.FragmentSpaceListBinding import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseFragment -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity import net.opendasharchive.openarchive.services.gdrive.GDriveActivity -import net.opendasharchive.openarchive.services.webdav.WebDavActivity import org.koin.compose.viewmodel.koinViewModel class SpaceListFragment : BaseFragment() { - private lateinit var binding: FragmentSpaceListBinding companion object { const val EXTRA_DATA_SPACE = "space_id" @@ -31,30 +29,27 @@ class SpaceListFragment : BaseFragment() { savedInstanceState: Bundle? ): View { - binding = FragmentSpaceListBinding.inflate(inflater) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val viewModel: SpaceListViewModel = koinViewModel() - binding.composeViewSpaceList.setContent { + SaveAppTheme { - val viewModel: SpaceListViewModel = koinViewModel() + // Calling refresh here will update state & trigger recomposition + LaunchedEffect(Unit) { + viewModel.refreshSpaces() + } - SaveAppTheme { - - // Calling refresh here will update state & trigger recomposition - LaunchedEffect(Unit) { - viewModel.refreshSpaces() + SpaceListScreen( + onSpaceClicked = { space -> + startSpaceAuthActivity(space.id) + }, + ) } - - SpaceListScreen( - onSpaceClicked = { space -> - startSpaceAuthActivity(space.id) - }, - ) } - } - - return binding.root } override fun getToolbarTitle() = getString(R.string.pref_title_media_servers) @@ -64,9 +59,8 @@ class SpaceListFragment : BaseFragment() { when (space.tType) { Space.Type.INTERNET_ARCHIVE -> { - val intent = Intent(requireContext(), InternetArchiveActivity::class.java) - intent.putExtra(EXTRA_DATA_SPACE, space.id) - startActivity(intent) + val action = SpaceListFragmentDirections.actionFragmentSpaceListToFragmentInternetArchiveDetail(spaceId = spaceId) + findNavController().navigate(action) } Space.Type.GDRIVE -> { @@ -82,9 +76,8 @@ class SpaceListFragment : BaseFragment() { } else -> { - val intent = Intent(requireContext(), WebDavActivity::class.java) - intent.putExtra(EXTRA_DATA_SPACE, space.id) - startActivity(intent) + val action = SpaceListFragmentDirections.actionFragmentSpaceListToFragmentWebDav(spaceId = spaceId) + findNavController().navigate(action) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/services/Module.kt new file mode 100644 index 00000000..38b18006 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/services/Module.kt @@ -0,0 +1,11 @@ +package net.opendasharchive.openarchive.services + +import net.opendasharchive.openarchive.services.tor.torModule +import org.koin.dsl.module + +internal val servicesModule = module { + + factory { SaveClient(get()) } + + includes(torModule) +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt index 1dc4be86..e8859751 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt @@ -3,146 +3,92 @@ package net.opendasharchive.openarchive.services import android.content.Context import android.content.Intent import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine -import info.guardianproject.netcipher.client.StrongBuilder -import info.guardianproject.netcipher.client.StrongBuilderBase +import info.guardianproject.netcipher.client.StrongOkHttpClientBuilder import info.guardianproject.netcipher.proxy.OrbotHelper -import net.opendasharchive.openarchive.R +import info.guardianproject.netcipher.proxy.OrbotHelper.SimpleStatusCallback +import net.opendasharchive.openarchive.core.infrastructure.client.enqueueResult import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.services.webdav.BasicAuthInterceptor import net.opendasharchive.openarchive.util.Prefs +import okhttp3.Call import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request -import okhttp3.internal.platform.Platform +import okhttp3.Response +import org.koin.core.component.KoinComponent +import timber.log.Timber import java.util.concurrent.TimeUnit -import kotlin.coroutines.suspendCoroutine -class SaveClient(context: Context) : StrongBuilderBase(context) { - - class OrbotException(message: String): Exception(message) +class SaveClient(private val context: Context) : SimpleStatusCallback(), KoinComponent, Call.Factory { private var okBuilder: OkHttpClient.Builder + private val strongBuilder: StrongOkHttpClientBuilder init { + okBuilder = setup() + strongBuilder = StrongOkHttpClientBuilder.forMaxSecurity(context) + if (Prefs.useTor) { + OrbotHelper.get(context).apply { + addStatusCallback(this@SaveClient) + init() + } + } + } + + private fun setup(): OkHttpClient.Builder { val cacheInterceptor = Interceptor { chain -> val request = chain.request().newBuilder().addHeader("Connection", "close").build() chain.proceed(request) } - okBuilder = OkHttpClient.Builder() + var builder = OkHttpClient.Builder() .addInterceptor(cacheInterceptor) .connectTimeout(40L, TimeUnit.SECONDS) .writeTimeout(40L, TimeUnit.SECONDS) .readTimeout(40L, TimeUnit.SECONDS) .retryOnConnectionFailure(false) .protocols(arrayListOf(Protocol.HTTP_1_1)) - } - /** - * OkHttp3 [does not support SOCKS proxies.](https://github.com/square/okhttp/issues/2315) - * - * @return false - */ - override fun supportsSocksProxy(): Boolean { - return false + return builder } - /** - * {@inheritDoc} - */ - override fun build(status: Intent): OkHttpClient { - if (!status.hasExtra(OrbotHelper.EXTRA_STATUS)) { - status.putExtra(OrbotHelper.EXTRA_STATUS, OrbotHelper.STATUS_OFF) - } + override fun onEnabled(statusIntent: Intent?) { + OrbotHelper.get(context).removeStatusCallback(this) - return applyTo(okBuilder, status).build() - } + if (Prefs.useTor.not()) return - /** - * Adds NetCipher configuration to an existing OkHttpClient.Builder, - * in case you have additional configuration that you wish to - * perform. - * - * @param builder a new or partially-configured OkHttpClient.Builder - * @return the same builder - */ - private fun applyTo(builder: OkHttpClient.Builder, status: Intent?): OkHttpClient.Builder { - val factory = buildSocketFactory() - - if (factory != null) { - val trustManager = Platform.get().trustManager(factory) - - if (trustManager != null) { - builder.sslSocketFactory(factory, trustManager) - } + try { + strongBuilder.applyTo(okBuilder, statusIntent) + } catch (e: Exception) { + Timber.e(e, "Error setting up OkHttp client") } - - return builder - .proxy(buildProxy(status)) } - @Throws(Exception::class) - override fun get(status: Intent, connection: OkHttpClient, url: String): String? { - val request: Request = Request.Builder().url(TOR_CHECK_URL).build() - - return connection.newCall(request).execute().body?.string() + override fun onNotYetInstalled() { + OrbotHelper.get(context).removeStatusCallback(this) + okBuilder = okBuilder.proxy(null) } - companion object { - suspend fun get(context: Context, user: String = "", password: String = ""): OkHttpClient { - - val strongBuilder = SaveClient(context) + override fun onStatusTimeout() { + OrbotHelper.get(context).removeStatusCallback(this) + okBuilder = okBuilder.proxy(null) + } - if (user.isNotEmpty() || password.isNotEmpty()) { - strongBuilder.okBuilder.addInterceptor(BasicAuthInterceptor(user, password)) - } + override fun newCall(request: Request): Call { + return okBuilder.build().newCall(request) + } - return suspendCoroutine { - val callback = object : StrongBuilder.Callback { - override fun onConnected(connection: OkHttpClient?) { - val result = if (connection != null) { - Result.success(connection) - } - else { - Result.failure(OrbotException(context.getString(R.string.tor_connection_exception))) - } - - it.resumeWith(result) - } - - override fun onConnectionException(e: java.lang.Exception?) { - it.resumeWith(Result.failure(e ?: OrbotException(context.getString(R.string.tor_connection_exception)))) - } - - override fun onTimeout() { - it.resumeWith(Result.failure(OrbotException(context.getString(R.string.tor_connection_timeout)))) - } - - override fun onInvalid() { - it.resumeWith(Result.failure(OrbotException(context.getString(R.string.tor_connection_invalid)))) - } - } - - if (Prefs.useTor) { - if (!OrbotHelper.requestStartTor(context)) { - callback.onInvalid() - } - else { - strongBuilder.build(callback) - } - } - else { - callback.onConnected(strongBuilder.build(Intent())) - } - } - } + suspend fun enqueue(request: Request): Result { + return okBuilder.build().enqueueResult(request) + } - suspend fun getSardine(context: Context, space: Space): OkHttpSardine { - val sardine = OkHttpSardine(get(context)) - sardine.setCredentials(space.username, space.password) + fun execute(request: Request): Response { + return okBuilder.build().newCall(request).execute() + } - return sardine - } + fun webdav(space: Space): OkHttpSardine { + val sardine = OkHttpSardine(okBuilder.build()) + sardine.setCredentials(space.username, space.password) + return sardine } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveConduit.kt index a28d025f..667323dc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/gdrive/GDriveConduit.kt @@ -20,7 +20,7 @@ import com.google.api.services.drive.model.File import info.guardianproject.netcipher.proxy.OrbotHelper import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.db.Media -import net.opendasharchive.openarchive.features.folders.Folder +import net.opendasharchive.openarchive.features.folders.BrowseFoldersViewModel.Folder import net.opendasharchive.openarchive.services.Conduit import net.opendasharchive.openarchive.util.Prefs import org.apache.http.conn.ClientConnectionManager diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt index 7ac8a122..e2c386cc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt @@ -3,19 +3,21 @@ package net.opendasharchive.openarchive.services.internetarchive import android.content.Context import android.net.Uri import com.google.gson.GsonBuilder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.db.Media import net.opendasharchive.openarchive.services.Conduit import net.opendasharchive.openarchive.services.SaveClient import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import java.io.File import java.io.IOException +import androidx.core.net.toUri -class IaConduit(media: Media, context: Context) : Conduit(media, context) { +class IaConduit(media: Media, context: Context) : Conduit(media, context), KoinComponent { + private val client: SaveClient by inject() companion object { const val ARCHIVE_BASE_URL = "https://archive.org/" @@ -39,8 +41,6 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { try { val mimeType = mMedia.mimeType - val client = SaveClient.get(mContext) - val fileName = getUploadFileName(mMedia, true) val metaJson = gson.toJson(mMedia) // Commenting out proof generation - 17th April 2025 @@ -80,14 +80,14 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { // Ignored. Not used here. } - private suspend fun OkHttpClient.uploadContent(fileName: String, mimeType: String) { + private fun SaveClient.uploadContent(fileName: String, mimeType: String) { val mediaUri = mMedia.originalFilePath val url = "${ARCHIVE_API_ENDPOINT}/${mMedia.serverUrl}/$fileName" val requestBody = RequestBodyUtil.create( mContext.contentResolver, - Uri.parse(mediaUri), + mediaUri.toUri(), mMedia.contentLength, mimeType.toMediaTypeOrNull(), createListener( @@ -108,7 +108,7 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { } @Throws(IOException::class) - private fun OkHttpClient.uploadMetaData(content: String, fileName: String) { + private suspend fun SaveClient.uploadMetaData(content: String, fileName: String) { val requestBody = RequestBodyUtil.create( textMediaType, content.byteInputStream(), @@ -129,7 +129,7 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { /// upload proof mode @Throws(IOException::class) - private fun OkHttpClient.uploadProofFiles(uploadFile: File) { + private suspend fun SaveClient.uploadProofFiles(uploadFile: File) { val requestBody = RequestBodyUtil.create( mContext.contentResolver, Uri.fromFile(uploadFile), @@ -225,33 +225,6 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { .build() } - @Throws(Exception::class) - private suspend fun OkHttpClient.execute(request: Request) = withContext(Dispatchers.IO) { - val result = newCall(request) - .execute() - - if (result.isSuccessful.not()) { - throw RuntimeException("${result.code}: ${result.message}") - } - } - - @Throws(Exception::class) - private fun OkHttpClient.enqueue(request: Request) { - newCall(request) - .enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - jobFailed(e) - } - - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - jobFailed(Exception("${response.code}: ${response.message}")) - } - } - - }) - } - private fun sanitizeHeaderValue(value: String): String { return value.replace("[^\\x20-\\x7E]".toRegex(), "") // Removes non-ASCII characters } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/tor/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/services/tor/Module.kt new file mode 100644 index 00000000..24804fdc --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/services/tor/Module.kt @@ -0,0 +1,10 @@ +package net.opendasharchive.openarchive.services.tor + +import org.koin.core.module.dsl.viewModel +import org.koin.core.qualifier.named +import org.koin.dsl.module + +internal val torModule = module { + single(named("tor")) { TorRepository() } + viewModel { TorViewModel(get(), get(named("tor"))) } +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorRepository.kt new file mode 100644 index 00000000..d5c58899 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorRepository.kt @@ -0,0 +1,40 @@ +package net.opendasharchive.openarchive.services.tor + +import android.content.Intent +import info.guardianproject.netcipher.proxy.StatusCallback +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface ITorRepository : StatusCallback { + val torStatus: StateFlow +} + +class TorRepository() : ITorRepository { + private val _torStatus = MutableStateFlow(TorStatus.DISCONNECTED) + override val torStatus: StateFlow = _torStatus.asStateFlow() + override fun onEnabled(p0: Intent?) { + _torStatus.value = TorStatus.CONNECTED + } + + override fun onStarting() { + _torStatus.value = TorStatus.CONNECTING + } + + override fun onStopping() { + _torStatus.value = TorStatus.DISCONNECTING + } + + override fun onDisabled() { + _torStatus.value = TorStatus.DISCONNECTED + } + + override fun onStatusTimeout() { + _torStatus.value = TorStatus.DISCONNECTED + } + + override fun onNotYetInstalled() { + _torStatus.value = TorStatus.DISCONNECTED + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorStatus.kt b/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorStatus.kt new file mode 100644 index 00000000..ea7c79ac --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorStatus.kt @@ -0,0 +1,8 @@ +package net.opendasharchive.openarchive.services.tor + +enum class TorStatus { + DISCONNECTED, + CONNECTING, + CONNECTED, + DISCONNECTING, +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorViewModel.kt new file mode 100644 index 00000000..426663d4 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/services/tor/TorViewModel.kt @@ -0,0 +1,71 @@ +package net.opendasharchive.openarchive.services.tor + +import android.app.Activity +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import info.guardianproject.netcipher.proxy.OrbotHelper +import info.guardianproject.netcipher.proxy.OrbotHelper.InstallCallback +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber + + +class TorViewModel( + private val application: Application, + torRepository: ITorRepository, +) : AndroidViewModel(application), InstallCallback { + + init { + OrbotHelper.get(application).addStatusCallback(torRepository) + } + val torStatus: StateFlow = torRepository.torStatus + + fun toggleTorServiceState(activity: Activity, enabled: Boolean) { + if (enabled) { + startTor(activity) + } + } + + fun requestOpenOrInstallOrbot(activity: Activity) { + if (OrbotHelper.isOrbotInstalled(application)) { + requestOpenOrbot(activity) + } else { + OrbotHelper.get(application).apply { + addInstallCallback(this@TorViewModel) + installOrbot(activity) + } + } + } + + private fun startTor(activity: Activity) { + if (OrbotHelper.isOrbotInstalled(application)) { + OrbotHelper.requestStartTor(application) + } else { + OrbotHelper.get(application).addInstallCallback(this) + OrbotHelper.get(application).installOrbot(activity) + } + } + + private fun requestOpenOrbot(activity: Activity): Boolean { + if (!OrbotHelper.isOrbotInstalled(application)) { + return false + } + val intent = + activity.packageManager.getLaunchIntentForPackage(OrbotHelper.ORBOT_PACKAGE_NAME) + if (intent == null) { + Timber.e("Orbot is not installed.") + return false + } + + activity.startActivity(intent) + return true + } + + override fun onInstalled() { + OrbotHelper.get(application).removeInstallCallback(this) + OrbotHelper.requestStartTor(application) + } + + override fun onInstallTimeout() { + Timber.e("timeout on orbot install") + } +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/BasicAuthInterceptor.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/BasicAuthInterceptor.kt deleted file mode 100644 index 8f956d75..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/BasicAuthInterceptor.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.opendasharchive.openarchive.services.webdav - -import okhttp3.Credentials.basic -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import java.io.IOException -import kotlin.Throws - -class BasicAuthInterceptor(user: String?, password: String?) : Interceptor { - private val credentials: String - @Throws(IOException::class) - override fun intercept(chain: Interceptor.Chain): Response { - val request: Request = chain.request() - val authenticatedRequest = request.newBuilder() - .header("Authorization", credentials).build() - return chain.proceed(authenticatedRequest) - } - init { - credentials = basic(user!!, password!!) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavActivity.kt deleted file mode 100644 index 22625ab7..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavActivity.kt +++ /dev/null @@ -1,71 +0,0 @@ -package net.opendasharchive.openarchive.services.webdav - -import android.content.Intent -import android.os.Bundle -import android.view.MenuItem -import androidx.fragment.app.commit -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.databinding.ActivityWebdavBinding -import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.core.BaseActivity -import net.opendasharchive.openarchive.features.main.MainActivity -import kotlin.properties.Delegates - -class WebDavActivity : BaseActivity() { - - companion object { - const val FRAGMENT_TAG = "webdav_fragment" - } - - private lateinit var mBinding: ActivityWebdavBinding - private var mSpaceId by Delegates.notNull() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - mBinding = ActivityWebdavBinding.inflate(layoutInflater) - setContentView(mBinding.root) - - setupToolbar(title = getString(R.string.edit_private_server), showBackButton = true) - - mSpaceId = intent.getLongExtra(EXTRA_DATA_SPACE, WebDavFragment.ARG_VAL_NEW_SPACE) - - if (mSpaceId != WebDavFragment.ARG_VAL_NEW_SPACE) { - supportFragmentManager.commit { - replace(mBinding.webDavFragment.id, WebDavFragment.newInstance(mSpaceId)) - } - } - - supportFragmentManager.setFragmentResultListener(WebDavFragment.RESP_SAVED, this) { _, _ -> - finishAffinity() - startActivity(Intent(this, MainActivity::class.java)) - } - - supportFragmentManager.setFragmentResultListener(WebDavFragment.RESP_DELETED, this) { _, _ -> - Space.navigate(this) - } - - supportFragmentManager.setFragmentResultListener(WebDavFragment.RESP_LICENSE, this) { _, _ -> - // Navigate to license fragment - // also update title with server name if available - like breadcrumb - supportFragmentManager - .beginTransaction() - .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left) - .replace( - mBinding.webDavFragment.id, - WebDavSetupLicenseFragment.newInstance(spaceId = mSpaceId, isEditing = true), - FRAGMENT_TAG, - ) - .commit() - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - // handle appbar back button tap - if (item.itemId == android.R.id.home) { - finish() - return true - } - return super.onOptionsItemSelected(item) - } -} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt index e95860e7..de631794 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavConduit.kt @@ -8,21 +8,23 @@ import net.opendasharchive.openarchive.db.Media import net.opendasharchive.openarchive.services.Conduit import net.opendasharchive.openarchive.services.SaveClient import okhttp3.HttpUrl +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import java.io.IOException -import java.util.* -class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { +class WebDavConduit(media: Media, context: Context) : Conduit(media, context), KoinComponent { - private lateinit var mClient: OkHttpSardine + private val client: SaveClient by inject() + + private val space = mMedia.space!! + + private var webdav = client.webdav(space) override suspend fun upload(): Boolean { - val space = mMedia.space ?: return false val base = space.hostUrl ?: return false val path = getPath() ?: return false - mClient = SaveClient.getSardine(mContext, space) - sanitize() val fileName = getUploadFileName(mMedia) @@ -30,7 +32,7 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { try { createFolders(base, path) - uploadMetadata(base, path, fileName) + webdav.uploadMetadata(base, path, fileName) } catch (e: Throwable) { jobFailed(e) @@ -44,14 +46,14 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { AppLogger.i("Begin media file upload...") if (mMedia.contentLength > CHUNK_FILESIZE_THRESHOLD) { - return uploadChunked(base, path, fileName) + return webdav.uploadChunked(base, path, fileName) } val fullPath = construct(base, path, fileName) AppLogger.i("Uploading started for single file upload...", "filePath: $fullPath") try { - mClient.put(mContext.contentResolver, + webdav.put(mContext.contentResolver, fullPath, mMedia.fileUri, mMedia.contentLength, @@ -87,16 +89,17 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { } override suspend fun createFolder(url: String) { - if (!mClient.exists(url)) { - mClient.createDirectory(url) + if (!webdav.exists(url)) { + webdav.createDirectory(url) } else { AppLogger.i("folder already exists: ", url) } } @Throws(IOException::class) - private suspend fun uploadChunked(base: HttpUrl, path: List, fileName: String): Boolean { + private suspend fun OkHttpSardine.uploadChunked(base: HttpUrl, path: List, fileName: String): Boolean { AppLogger.i("Uploading started as chunked upload...") + val space = mMedia.space ?: return false val url = space.hostUrl ?: return false @@ -135,17 +138,17 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { val total = offset + length val chunkPath = construct(tmpBase, tmpPath, "$offset-$total") - val chunkExists = mClient.exists(chunkPath) + val chunkExists = exists(chunkPath) var chunkLengthMatches = false if (chunkExists) { - val dirList = mClient.list(chunkPath) + val dirList = list(chunkPath) chunkLengthMatches = !dirList.isNullOrEmpty() && dirList.first().contentLength == length.toLong() } if (!chunkExists || !chunkLengthMatches) { - mClient.put( + put( chunkPath, buffer, mMedia.mimeType, @@ -170,7 +173,7 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { val dest = mutableListOf("files", space.username) dest.addAll(path) - mClient.move(construct(tmpBase, tmpPath, ".file"), construct(tmpBase, dest, fileName)) + move(construct(tmpBase, tmpPath, ".file"), construct(tmpBase, dest, fileName)) mMedia.serverUrl = construct(base, path, fileName) @@ -185,13 +188,13 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { } } - private fun uploadMetadata(base: HttpUrl, path: List, fileName: String) { + private fun OkHttpSardine.uploadMetadata(base: HttpUrl, path: List, fileName: String) { AppLogger.i("Uploading metadata....") val metadata = getMetadata() if (mCancelled) throw Exception("Cancelled") - mClient.put( + put( construct(base, path, "$fileName.meta.json"), metadata.toByteArray(), "text/plain", @@ -202,7 +205,7 @@ class WebDavConduit(media: Media, context: Context) : Conduit(media, context) { for (file in getProof()) { if (mCancelled) throw Exception("Cancelled") - mClient.put( + put( construct(base, path, file.name), file, "text/plain", false, null) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt index ff7288df..37ad2881 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/WebDavFragment.kt @@ -45,8 +45,10 @@ import net.opendasharchive.openarchive.util.extensions.makeSnackBar import net.opendasharchive.openarchive.util.extensions.show import okhttp3.Call import okhttp3.Callback +import okhttp3.Credentials import okhttp3.Request import okhttp3.Response +import org.koin.java.KoinJavaComponent.inject import java.io.IOException import kotlin.coroutines.suspendCoroutine @@ -60,6 +62,8 @@ class WebDavFragment : BaseFragment() { private var originalName: String? = null private var isNameChanged = false + private val client: SaveClient by inject(SaveClient::class.java) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mSpaceId = arguments?.getLong(ARG_SPACE_ID) ?: ARG_VAL_NEW_SPACE @@ -429,10 +433,9 @@ class WebDavFragment : BaseFragment() { private suspend fun testConnection() { val url = mSpace.hostUrl ?: throw IOException("400 Bad Request") - val client = SaveClient.get(requireContext(), mSpace.username, mSpace.password) - val request = Request.Builder().url(url).method("GET", null).addHeader("OCS-APIRequest", "true") + .addHeader("Authorization", Credentials.basic(mSpace.username, mSpace.password)) .addHeader("Accept", "application/json").build() return suspendCoroutine { diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt deleted file mode 100644 index f56093dd..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerActivity.kt +++ /dev/null @@ -1,151 +0,0 @@ -package net.opendasharchive.openarchive.upload - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.view.Menu -import android.view.MenuItem -import net.opendasharchive.openarchive.CleanInsightsManager -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.databinding.ActivityUploadManagerBinding -import net.opendasharchive.openarchive.db.Media -import net.opendasharchive.openarchive.features.core.BaseActivity - -class UploadManagerActivity : BaseActivity() { - - private lateinit var mBinding: ActivityUploadManagerBinding - var mFrag: UploadManagerFragment? = null - private var mMenuEdit: MenuItem? = null - - private val mMessageReceiver: BroadcastReceiver = object : BroadcastReceiver() { - private val handler = Handler(Looper.getMainLooper()) - - override fun onReceive(context: Context, intent: Intent) { - val action = BroadcastManager.getAction(intent) - val mediaId = action?.mediaId ?: return - - if (mediaId > -1) { - val media = Media.get(mediaId) - - if (action == BroadcastManager.Action.Delete || media?.sStatus == Media.Status.Uploaded) { - handler.post { mFrag?.removeItem(mediaId) } - } - else { - handler.post { mFrag?.updateItem(mediaId) } - } - } - } - } - - private var mEditMode = true // Setting Edit mode as the default mode - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - mBinding = ActivityUploadManagerBinding.inflate(layoutInflater) - setContentView(mBinding.root) - - setupToolbar( - title = getString(R.string.upload_manager_screen_title), - subtitle = getString(R.string.upload_manager_screen_subtitle), - showBackButton = true - ) - - //mFrag = supportFragmentManager.findFragmentById(R.id.fragUploadManager) as? UploadManagerFragment - - val bottomSheet = UploadManagerFragment() - bottomSheet.show(supportFragmentManager, UploadManagerFragment.TAG) - } - - override fun onResume() { - super.onResume() - mFrag?.refresh() - - BroadcastManager.register(this, mMessageReceiver) - - onStartEdit() - } - - override fun onPause() { - super.onPause() - - BroadcastManager.unregister(this, mMessageReceiver) - } - - private fun onStartEdit() { - UploadService.stopUploadService(this) - } - - private fun onCompleteEdit() { - UploadService.startUploadService(this) - } - - private fun toggleEditMode() { - mEditMode = !mEditMode - - mFrag?.refresh() - - if (mEditMode) { - mMenuEdit?.setTitle(R.string.menu_done) - - UploadService.stopUploadService(this) - } - else { - mMenuEdit?.setTitle(R.string.edit) - - UploadService.startUploadService(this) - } - - updateTitle() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_upload, menu) - mMenuEdit = menu.findItem(R.id.menu_done) - - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - finish() - return true - } - R.id.menu_done -> { - onCompleteEdit() - return true - } - } - return super.onOptionsItemSelected(item) - } - - override fun finish() { - // If we're still in edit mode, restart the upload service when the user leaves. - if (mEditMode) { - UploadService.startUploadService(this) - } - - super.finish() - } - - private fun updateTitle() { - if (mEditMode) { - supportActionBar?.title = getString(R.string.edit_media) - supportActionBar?.subtitle = getString(R.string.uploading_is_paused) - } else { - val count = mFrag?.getUploadingCounter() ?: 0 - - supportActionBar?.title = if (count < 1) { - getString(R.string.uploads) - } else { - getString(R.string.uploading_left, count) - } - - supportActionBar?.subtitle = null - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt index 02054497..929b811c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt @@ -14,6 +14,7 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.Configuration +import info.guardianproject.netcipher.proxy.OrbotHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -23,11 +24,17 @@ import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.db.Media import net.opendasharchive.openarchive.features.main.MainActivity import net.opendasharchive.openarchive.services.Conduit +import net.opendasharchive.openarchive.services.tor.ITorRepository +import net.opendasharchive.openarchive.services.tor.TorStatus import net.opendasharchive.openarchive.util.Prefs +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.qualifier.named +import timber.log.Timber import java.io.IOException import java.util.* -class UploadService : JobService() { +class UploadService : JobService(), KoinComponent { companion object { private const val MY_BACKGROUND_JOB = 0 @@ -56,6 +63,8 @@ class UploadService : JobService() { private var mRunning = false private var mKeepUploading = true private val mConduits = ArrayList() + private lateinit var notification: Notification + private val torRepo: ITorRepository by inject(named("tor")) override fun onCreate() { super.onCreate() @@ -200,6 +209,10 @@ class UploadService : JobService() { return false } + private fun isTorAvailable(): Boolean { + return torRepo.torStatus.value == TorStatus.CONNECTED + } + private fun isNetworkAvailable(requireUnmetered: Boolean): Boolean { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/extensions/PackageManager.kt b/app/src/main/java/net/opendasharchive/openarchive/util/extensions/PackageManager.kt index 59ff2771..796e9436 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/util/extensions/PackageManager.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/util/extensions/PackageManager.kt @@ -3,7 +3,7 @@ package net.opendasharchive.openarchive.util.extensions import android.content.pm.PackageManager import android.os.Build -fun PackageManager.getVersionName(packageName: String): String { +fun PackageManager.getVersionName(packageName: String): String? { return try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) @@ -12,7 +12,7 @@ fun PackageManager.getVersionName(packageName: String): String { getPackageInfo(packageName, 0) }.versionName - } catch (e: PackageManager.NameNotFoundException) { + } catch (_: PackageManager.NameNotFoundException) { "unknown" } } diff --git a/app/src/main/res/layout/activity_add_folder.xml b/app/src/main/res/layout/activity_add_folder.xml deleted file mode 100644 index 42438c74..00000000 --- a/app/src/main/res/layout/activity_add_folder.xml +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings_container.xml b/app/src/main/res/layout/activity_proofmode_settings.xml similarity index 96% rename from app/src/main/res/layout/activity_settings_container.xml rename to app/src/main/res/layout/activity_proofmode_settings.xml index 0392c67a..f81cec18 100644 --- a/app/src/main/res/layout/activity_settings_container.xml +++ b/app/src/main/res/layout/activity_proofmode_settings.xml @@ -7,8 +7,7 @@ android:layout_height="match_parent" android:filterTouchesWhenObscured="true" android:gravity="center_horizontal" - android:orientation="vertical" - tools:context=".features.settings.GeneralSettingsActivity"> + android:orientation="vertical"> - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_webdav.xml b/app/src/main/res/layout/activity_webdav.xml deleted file mode 100644 index 15d46d7f..00000000 --- a/app/src/main/res/layout/activity_webdav.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/content_upload_manager.xml b/app/src/main/res/layout/content_upload_manager.xml deleted file mode 100644 index 66c4f746..00000000 --- a/app/src/main/res/layout/content_upload_manager.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/activity_edit_folder.xml b/app/src/main/res/layout/fragment_edit_folder.xml similarity index 94% rename from app/src/main/res/layout/activity_edit_folder.xml rename to app/src/main/res/layout/fragment_edit_folder.xml index 125ab7ce..db661aa5 100644 --- a/app/src/main/res/layout/activity_edit_folder.xml +++ b/app/src/main/res/layout/fragment_edit_folder.xml @@ -7,12 +7,7 @@ android:filterTouchesWhenObscured="true" android:orientation="vertical" tools:background="@color/colorBackground" - tools:context=".features.settings.EditFolderActivity"> - - - + tools:context=".features.settings.EditFolderFragment"> + app:layout_constraintTop_toTopOf="parent" /> - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_folders.xml b/app/src/main/res/layout/fragment_folder_list.xml similarity index 55% rename from app/src/main/res/layout/activity_folders.xml rename to app/src/main/res/layout/fragment_folder_list.xml index 4ef843c1..f6cacaca 100644 --- a/app/src/main/res/layout/activity_folders.xml +++ b/app/src/main/res/layout/fragment_folder_list.xml @@ -1,54 +1,40 @@ + tools:context=".features.settings.FolderListFragment"> + + android:orientation="vertical"> - - - - + android:layout_margin="@dimen/activity_vertical_margin" + android:layout_marginHorizontal="8dp" /> + android:textSize="18sp" + tools:visibility="gone" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_space_list.xml b/app/src/main/res/layout/fragment_space_list.xml deleted file mode 100644 index 396fb067..00000000 --- a/app/src/main/res/layout/fragment_space_list.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/prefs_open_orbot.xml b/app/src/main/res/layout/prefs_open_orbot.xml new file mode 100644 index 00000000..ec556ffc --- /dev/null +++ b/app/src/main/res/layout/prefs_open_orbot.xml @@ -0,0 +1,14 @@ + + +