diff --git a/app/build.gradle b/app/build.gradle index e97066563..4936aa2db 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ import plugin.KiwixConfigurationPlugin plugins { id("com.android.application") + id("com.github.triplet.play") version("2.5.0") } apply plugin: KiwixConfigurationPlugin @@ -67,6 +68,10 @@ android { } play { + enabled = true + serviceAccountCredentials = file("../google.json") + track = "alpha" + releaseStatus = "draft" resolutionStrategy = "fail" } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index fc9d49570..f7a9248da 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,7 +15,8 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50") implementation("com.dicedmelon.gradle:jacoco-android:0.1.4") implementation("org.jlleitschuh.gradle:ktlint-gradle:8.2.0") - implementation("com.github.triplet.gradle:play-publisher:2.5.0") + implementation("com.google.apis:google-api-services-androidpublisher:v3-rev129-1.25.0") + implementation(gradleApi()) implementation(localGroovy()) } diff --git a/buildSrc/src/main/kotlin/custom/Auth.kt b/buildSrc/src/main/kotlin/custom/Auth.kt new file mode 100644 index 000000000..1650acc7f --- /dev/null +++ b/buildSrc/src/main/kotlin/custom/Auth.kt @@ -0,0 +1,122 @@ +/* Kiwix Android + * Copyright (c) 2019 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package custom + +import com.android.build.gradle.api.ApkVariantOutput +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport +import com.google.api.client.http.FileContent +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.GenericJson +import com.google.api.client.json.jackson2.JacksonFactory +import com.google.api.services.androidpublisher.AndroidPublisher +import com.google.api.services.androidpublisher.AndroidPublisherScopes +import com.google.api.services.androidpublisher.model.ExpansionFile +import com.google.api.services.androidpublisher.model.ExpansionFilesUploadResponse +import com.google.api.services.androidpublisher.model.Track +import com.google.api.services.androidpublisher.model.TrackRelease +import java.io.File +import java.io.FileInputStream +import java.security.KeyStore + +fun createPublisher(auth: File): AndroidPublisher { + val transport = buildTransport() + val factory = JacksonFactory.getDefaultInstance() + + val credential = + GoogleCredential.fromStream(auth.inputStream(), transport, factory) + .createScoped(listOf(AndroidPublisherScopes.ANDROIDPUBLISHER)) + + + return AndroidPublisher.Builder(transport, JacksonFactory.getDefaultInstance()) { + credential.initialize(it.setReadTimeout(0)) + }.setApplicationName("kiwixcustom").build() +} + +private fun buildTransport(): NetHttpTransport { + val trustStore: String? = System.getProperty("javax.net.ssl.trustStore", null) + val trustStorePassword: String? = + System.getProperty("javax.net.ssl.trustStorePassword", null) + + return if (trustStore == null) { + GoogleNetHttpTransport.newTrustedTransport() + } else { + val ks = KeyStore.getInstance(KeyStore.getDefaultType()) + FileInputStream(trustStore).use { fis -> + ks.load(fis, trustStorePassword?.toCharArray()) + } + NetHttpTransport.Builder().trustCertificates(ks).build() + } +} + +class Transaction( + private val publisher: AndroidPublisher, + private val packageName: String, + val editId: String +) { + fun uploadExpansionTo( + file: File, + apkVariantOutput: ApkVariantOutput + ): ExpansionFilesUploadResponse = publisher.edits().expansionfiles() + .upload( + packageName, + editId, + apkVariantOutput.versionCodeOverride, + "main", + FileContent("application/octet-stream", file) + ).execute().prettyPrint() + + fun attachExpansionTo(expansionCode: Int, apkVariantOutput: ApkVariantOutput): ExpansionFile = + publisher.edits().expansionfiles().update( + packageName, + editId, + apkVariantOutput.versionCodeOverride, + "main", + ExpansionFile().apply { referencesVersion = expansionCode } + ).execute().prettyPrint() + + fun uploadApk(apkVariantOutput: ApkVariantOutput) { + publisher.edits().apks().upload( + packageName, + editId, + FileContent("application/octet-stream", apkVariantOutput.outputFile) + ).execute().prettyPrint() + } + + fun addToTrackInDraft(apkVariants: List): Track = + publisher.edits().tracks().update(packageName, editId, "alpha", Track().apply { + releases = listOf(TrackRelease().apply { + status = "draft" + name = apkVariants[0].versionNameOverride + versionCodes = apkVariants.map { it.versionCodeOverride.toLong() } + }) + track = "alpha" + }).execute().prettyPrint() +} + +fun AndroidPublisher.transactionWithCommit(packageName: String, func: Transaction.() -> Unit) { + Transaction( + this, + packageName, + edits().insert(packageName, null).execute().prettyPrint().id + ).apply { + func() + edits().validate(packageName, editId).execute().prettyPrint() + }.also { edits().commit(packageName, it.editId).execute().prettyPrint() } +} + +private fun T.prettyPrint() = also { println(it.toPrettyString()) } diff --git a/buildSrc/src/main/kotlin/plugin/AppConfigurer.kt b/buildSrc/src/main/kotlin/plugin/AppConfigurer.kt index 0c7256e31..a749ff24c 100644 --- a/buildSrc/src/main/kotlin/plugin/AppConfigurer.kt +++ b/buildSrc/src/main/kotlin/plugin/AppConfigurer.kt @@ -22,7 +22,6 @@ import Libs import com.android.build.VariantOutput import com.android.build.gradle.AppExtension import com.android.build.gradle.api.ApkVariantOutput -import com.github.triplet.gradle.play.PlayPublisherExtension import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.exclude @@ -37,7 +36,7 @@ class AppConfigurer { } signingConfigs { create("releaseSigningConfig") { - this.storeFile = File(target.rootDir, "kiwix-android.keystore") + storeFile = File(target.rootDir, "kiwix-android.keystore") storePassword = System.getenv("KEY_STORE_PASSWORD") ?: "000000" keyAlias = System.getenv("KEY_ALIAS") ?: "keystore" keyPassword = System.getenv("KEY_PASSWORD") ?: "000000" @@ -76,13 +75,6 @@ class AppConfigurer { cruncherEnabled = true } } - target.plugins.apply("com.github.triplet.play") - target.configureExtension { - isEnabled = true - serviceAccountCredentials = File(target.rootDir, "google.json") - track = "alpha" - releaseStatus = "draft" - } configureDependencies(target) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/FetchDownloadDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/FetchDownloadDao.kt index af7bce18e..8fea7679e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/FetchDownloadDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/FetchDownloadDao.kt @@ -27,7 +27,6 @@ import org.kiwix.kiwixmobile.core.downloader.DownloadRequester import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book -import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity import org.kiwix.kiwixmobile.core.dao.entities.FetchDownloadEntity import org.kiwix.kiwixmobile.core.dao.entities.FetchDownloadEntity_ import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk @@ -79,7 +78,7 @@ class FetchDownloadDao @Inject constructor( } fun addIfDoesNotExist( - metaLinkNetworkEntity: MetaLinkNetworkEntity, + url: String, book: Book, downloadRequester: DownloadRequester ) { @@ -88,7 +87,7 @@ class FetchDownloadDao @Inject constructor( insert( downloadRequester.enqueue( DownloadRequest( - metaLinkNetworkEntity, + url, book ) ), diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloaderImpl.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloaderImpl.kt index c344dba72..20fa3ffc1 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloaderImpl.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloaderImpl.kt @@ -18,10 +18,12 @@ package org.kiwix.kiwixmobile.core.downloader +import io.reactivex.Observable import org.kiwix.kiwixmobile.core.dao.FetchDownloadDao import org.kiwix.kiwixmobile.core.data.remote.KiwixService -import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem +import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity +import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import javax.inject.Inject class DownloaderImpl @Inject constructor( @@ -31,7 +33,7 @@ class DownloaderImpl @Inject constructor( ) : Downloader { override fun download(book: LibraryNetworkEntity.Book) { - kiwixService.getMetaLinks(book.url) + urlProvider(book) .take(1) .subscribe( { @@ -41,6 +43,10 @@ class DownloaderImpl @Inject constructor( ) } + private fun urlProvider(book: Book): Observable = + if (book.url.endsWith("meta4")) kiwixService.getMetaLinks(book.url).map { it.relevantUrl.value } + else Observable.just(book.url) + override fun cancelDownload(downloadItem: DownloadItem) { downloadRequester.cancel(downloadItem) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadRequest.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadRequest.kt index db4def4f5..f88f38245 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadRequest.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadRequest.kt @@ -18,8 +18,7 @@ package org.kiwix.kiwixmobile.core.downloader.model import android.net.Uri -import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity -import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity +import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.StorageUtils @@ -31,14 +30,7 @@ data class DownloadRequest( val uri: Uri get() = Uri.parse(urlString) - constructor( - metaLinkNetworkEntity: MetaLinkNetworkEntity, - book: LibraryNetworkEntity.Book - ) : this( - metaLinkNetworkEntity.relevantUrl.value, - book.title, - book.description - ) + constructor(url: String, book: Book) : this(url, book.title, book.description) fun getDestination(sharedPreferenceUtil: SharedPreferenceUtil): String = "${sharedPreferenceUtil.prefStorage}/Kiwix/${ diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.java b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.java index 15b23d10b..e8d3e1d09 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.java +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.java @@ -1089,10 +1089,7 @@ public abstract class CoreMainActivity extends BaseActivity implements WebViewCa switch (requestCode) { case REQUEST_STORAGE_PERMISSION: { if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { - finish(); - Intent newZimFile = Intents.internal(CoreMainActivity.class); - newZimFile.setData(Uri.fromFile(file)); - startActivity(newZimFile); + openZimFile(file); } else { AlertDialog.Builder builder = new AlertDialog.Builder(this, dialogStyle()); builder.setMessage(getResources().getString(R.string.reboot_message)); diff --git a/custom/build.gradle.kts b/custom/build.gradle.kts index 5ff299c56..4fad7dfa8 100644 --- a/custom/build.gradle.kts +++ b/custom/build.gradle.kts @@ -1,5 +1,13 @@ +import com.android.build.gradle.api.ApkVariantOutput +import com.android.build.gradle.api.ApplicationVariant import com.android.build.gradle.internal.dsl.ProductFlavor +import custom.createPublisher +import custom.transactionWithCommit import plugin.KiwixConfigurationPlugin +import java.net.URI +import java.net.URL +import java.text.SimpleDateFormat +import java.util.Date plugins { android @@ -12,20 +20,128 @@ android { } flavorDimensions("default") + productFlavors { - // productFlavors { - // create("customexample") { - // versionName = "2017-07" - // versionCode = 1 - // applicationIdSuffix = ".kiwixcustomexample" - // configureStrings("Test Custom App") - // } - // } + create( + CustomFlavor( + flavorName = "customexample", + versionName = "2017-07", + url = "http://download.kiwix.org/zim/wikipedia_fr_test.zim", + enforcedLanguage = "en", + appName = "Test Custom App" + ), + CustomFlavor( + flavorName = "wikimed", + versionName = "2018-08", + url = "http://download.kiwix.org/zim/wikipedia_en_medicine_novid.zim", + enforcedLanguage = "en", + appName = "Medical Wikipedia" + ) + ) + all { + val zimFile = File("$projectDir/src", "$name/$name.zim") + createDownloadTask(zimFile) + createPublishApkWithExpansionTask(name, zimFile, applicationVariants) + } + } + splits { + abi { + isUniversalApk = false + } + } } -apply(from = File("dynamic_flavors.gradle")) +// apply(from = File("dynamic_flavors.gradle")) fun ProductFlavor.configureStrings(appName: String) { resValue("string", "app_name", appName) resValue("string", "app_search_string", "Search $appName") } + +fun ProductFlavor.fetchUrl(): String { + val urlConnection = + URI.create(buildConfigFields["ZIM_URL"]!!.value.replace("\"", "")).toURL() + .openConnection() + urlConnection.connect() + urlConnection.getInputStream() + return urlConnection + .getHeaderField("Location") + ?.replace("https", "http") + ?: urlConnection.url.toString() +} + +fun NamedDomainObjectContainer.create(vararg customFlavors: CustomFlavor) { + customFlavors.forEach { customFlavor -> + create(customFlavor.flavorName) { + versionName = customFlavor.versionName + versionCode = customFlavor.versionCode + applicationIdSuffix = ".kiwixcustom${customFlavor.flavorName}" + buildConfigField("String", "ZIM_URL", "\"${customFlavor.url}\"") + buildConfigField("String", "ENFORCED_LANG", "\"${customFlavor.enforcedLanguage}\"") + configureStrings(customFlavor.appName) + } + } +} + +data class CustomFlavor( + val flavorName: String, + val versionName: String, + val versionCode: Int = Date().let { + SimpleDateFormat("YYDDD0").format(it).toInt() + }, + val url: String, + val enforcedLanguage: String, + val appName: String +) + +fun ProductFlavor.createDownloadTask(file: File): Task { + return tasks.create("download${name.capitalize()}Zim") { + group = "Downloading" + doLast { + if (!file.exists()) { + file.createNewFile() + URL(fetchUrl()).openStream().use { + it.copyTo(file.outputStream()) + } + } + } + } +} + +fun ProductFlavor.createPublishApkWithExpansionTask( + flavorName: String, + file: File, + applicationVariants: DomainObjectSet +): Task { + return tasks.create("publish${flavorName.capitalize()}ReleaseApkWithExpansionFile") { + group = "publishing" + description = "Uploads ${flavorName.capitalize()} to the Play Console with an Expansion file" + doLast { + val packageName = "org.kiwix$applicationIdSuffix" + println("packageName $packageName") + val apkVariants = getApkVariants(applicationVariants, flavorName) + createPublisher(File(rootDir, "google.json")) + .transactionWithCommit(packageName) { + apkVariants.forEach(::uploadApk) + uploadExpansionTo(file, apkVariants[0]) + apkVariants.drop(1).forEach { + attachExpansionTo(apkVariants[0].versionCodeOverride, it) + } + addToTrackInDraft(apkVariants) + } + } + } +} +afterEvaluate { + tasks.filter { it.name.contains("ReleaseApkWithExpansionFile") }.forEach { + val flavorName = + it.name.substringAfter("publish").substringBefore("ReleaseApkWithExpansionFile") + it.dependsOn.add(tasks.getByName("download${flavorName}Zim")) + it.dependsOn.add(tasks.getByName("assemble${flavorName}Release")) + } +} + +fun getApkVariants(applicationVariants: DomainObjectSet, flavorName: String) = + applicationVariants.find { + it.name.contains("release", true) && it.name.contains(flavorName, true) + }!!.outputs.filterIsInstance().sortedBy { it.versionCodeOverride } diff --git a/custom/src/customexample/build.gradle b/custom/src/customexample/build.gradle deleted file mode 100644 index 4e88a5bcc..000000000 --- a/custom/src/customexample/build.gradle +++ /dev/null @@ -1,15 +0,0 @@ -android { - def f = file(buildscript.sourceFile.parent + "/wikipedia_fr_test_2017-07.zim"); - if (!f.exists()) { - URLConnection urlConnection = new URL('http://download.kiwix.org/zim/wikipedia_fr_test.zim'). - openConnection() - urlConnection.connect() - urlConnection.getInputStream() - String url = urlConnection.getURL().toString() - if (urlConnection.getHeaderField("Location") != null) { - url = urlConnection.getHeaderField("Location") - url = url.replace("https", "http") - } - new URL(url).withInputStream { i -> f.withOutputStream { it << i } } - } -} diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomMainActivity.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomMainActivity.kt index f4d0ff146..55b4a654d 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomMainActivity.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomMainActivity.kt @@ -18,11 +18,9 @@ package org.kiwix.kiwixmobile.custom.main -import android.content.Intent import android.os.Bundle import android.util.Log import android.view.Menu -import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.start import org.kiwix.kiwixmobile.core.main.CoreMainActivity @@ -34,7 +32,6 @@ import org.kiwix.kiwixmobile.custom.customActivityComponent import org.kiwix.kiwixmobile.custom.download.CustomDownloadActivity import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile -import java.io.File import java.util.Locale import javax.inject.Inject @@ -57,7 +54,7 @@ class CustomMainActivity : CoreMainActivity() { super.onCreate(savedInstanceState) requireEnforcedLanguage() customFileValidator.validate( - { + onFilesFound = { when (it) { is HasFile -> openZimFile(it.file) is HasBothFiles -> { @@ -66,7 +63,7 @@ class CustomMainActivity : CoreMainActivity() { } } }, - { + onNoFilesFound = { finish() start() } @@ -90,7 +87,7 @@ class CustomMainActivity : CoreMainActivity() { if (BuildConfig.ENFORCED_LANG.isNotEmpty() && BuildConfig.ENFORCED_LANG != currentLocaleCode) { LanguageUtils.handleLocaleChange(this, BuildConfig.ENFORCED_LANG) sharedPreferenceUtil.putPrefLanguage(BuildConfig.ENFORCED_LANG) - startActivity(Intent(this, this.javaClass)) + recreate() return true } return false @@ -99,12 +96,4 @@ class CustomMainActivity : CoreMainActivity() { override fun manageZimFiles(tab: Int) { TODO("not implemented") } - - companion object { - private fun getExpansionAPKFileName() = - "main.${BuildConfig.CONTENT_VERSION_CODE}.${CoreApp.getInstance().packageName}.obb" - - fun generateExpansionFilePath(fileName: String = getExpansionAPKFileName()) = - "${CoreApp.getInstance().obbDir}${File.separator}$fileName" - } }