diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e164d73..14e551f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,16 +1,22 @@ --- name: Bug report -about: Create a report to help us improve +about: Create a bug report to help us improve title: '' labels: bug assignees: adriangl --- -**Device and SW details (please complete the following information):** - - Device: [e.g. Google Pixel 3] - - OS: [e.g. Android 9] - - Library Version [e.g. 1.0.0] +**Device and SW details** +- Device: [e.g. Google Pixel 3] +- OS: [e.g. Android 9] +- Library Version [e.g. 1.0.0] + +**Conditions for the library to work** +- [ ] I have set the package name of the app to **exactly** the one I'd like to test in-app updates with. +- [ ] I have signed the app with the same key that I used to sign the app I want to test in-app updates with. +- [ ] I've ensured that any of my Google accounts in my test device has access to said app in Google Play Store. +- [ ] The Google Play Store displays updates for the app I want to use to test in-app updates with. **Summary and background of the bug** A clear and concise description of what the bug is. @@ -18,8 +24,8 @@ A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to '...' -2. Click on '....' -3. Scroll down to '....' +2. Click on '...' +3. Scroll down to '...' 4. See error Also attach notes or stack traces if applicable. diff --git a/.gitignore b/.gitignore index 6a02cb8..6a19dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -40,10 +40,10 @@ captures/ .idea/dictionaries .idea/libraries .idea/caches -.idea/codeStyles .idea/modules.xml .idea/misc.xml .idea/jarRepositories.xml +.idea/vcs.xml # Keystore files *.jks @@ -57,4 +57,7 @@ google-services.json # Freeline freeline.py freeline/ -freeline_project_description.json \ No newline at end of file +freeline_project_description.json + +# App configuration properties +app_config.properties \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..0d15693 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 367fdaf..de753af 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,13 @@ This utility library aims to help Android developers to use the [Google Play In-App Updates API](https://developer.android.com/guide/app-bundle/in-app-updates) in an easy way. -**It's highly encouraged that you first read the [Google Play In-App Updates API](https://developer.android.com/guide/app-bundle/in-app-updates) documentation before using this library in order to understand the core concepts of the library.** +> It's highly encouraged that you first read the [Google Play In-App Updates API](https://developer.android.com/guide/app-bundle/in-app-updates) documentation before using this library in order to understand the core concepts of the library. + +## Setting Up +In your main `build.gradle`, add [jitpack.io](https://jitpack.io/) repository in the `allprojects` block: + +
Groovy -## Installation -Add the following dependencies to your main `build.gradle`: ```groovy allprojects { repositories { @@ -14,45 +17,113 @@ allprojects { } } ``` +
-Add the following dependencies to your app's `build.gradle`: +
Kotlin -* For Gradle < 4.0 - ```groovy - dependencies { - compile "com.github.bq:android-app-updates-helper:1.0.2" +```kotlin +allprojects { + repositories { + maven(url = "https://jitpack.io") } - ``` +} +``` +
+ + +Add the following dependencies to your app or library's `build.gradle`: + +
Groovy + +```groovy +dependencies { + implementation "com.github.bq:android-app-updates-helper:1.0.2" +} +``` +
+ + +
Kotlin + +```kotlin +dependencies { + implementation("com.github.bq:android-app-updates-helper:1.0.2") +} +``` +
-* For Gradle 4.0+ - ```groovy - dependencies { - implementation "com.github.bq:android-app-updates-helper:1.0.2" +You'll also need to add support for Java 8 in your project. To do so: +
Groovy + +```groovy +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" } - ``` +} +``` +
-## Example usage +
Kotlin + +```kotlin +android { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} +``` +
+ +## How to use * Create a new _AppUpdatesHelper_. -* Start listening for app update changes with _AppUpdatesHelper.startListening()_, for example in _onCreate()_. -* Stop listening for app update changes with _AppUpdatesHelper.stopListening()_ in _onDestroy()_. +* Start listening for app update changes with _AppUpdatesHelper.startListening()_, for example in _Activity.onCreate()_ or in _Fragment.onViewCreated()_. +* Stop listening for app update changes with _AppUpdatesHelper.stopListening()_ in _Activity.onDestroy()_ or in _Fragment.onDestroyView()_. * Request app update information with _AppUpdatesHelper.getAppUpdateInfo()_. * Request a flexible or immediate update with _AppUpdatesHelper.startFlexibleUpdate()_ or _AppUpdatesHelper.startImmediateUpdate()_ Check the [example app](app) for more implementation details about [flexible](app/src/main/kotlin/com/bq/appupdateshelper/flexible/FlexibleUpdateActivity.kt) and [immediate](app/src/main/kotlin/com/bq/appupdateshelper/immediate/ImmediateUpdateActivity.kt) updates. -If you use the example app, don't forget the following things when testing: -* Change the package name of the example app to the one you'd like to test in-app updates with. -* Sign the example app with the same keys that you used to sign the app you want to test in-app updates with. -* If the app is not published yet, or you want to test with internal app sharing or closed tracks, ensure that any of your Google accounts in your device has access to said app in Google Play Store. - You can also use a [fake implementation](app/src/main/kotlin/com/bq/appupdateshelper/fake/FakeUpdateActivity.kt) to test in-app updates. +Keep in mind that you may not see in-app updates if these conditions don't match: +* The package name of the app is **exactly** the one you'd like to test in-app updates with. +* The app must be signed with the same keys that you used to sign the app you want to test in-app updates with. +* If the app is not published yet or you want to test with internal app sharing or closed tracks, +ensure that any of your Google accounts in your device has access to said app in Google Play Store. +* Check if the Google Play Store displays updates for the app you want to use to test in-app updates. + +Please ensure that all conditions apply when using this library in order to avoid unnecessary headaches. + +### Using the example app +In order to ease using the example app with the sample data of your own app, +you can create an `app_config.properties` file in the root of the project with the following content: +```properties +applicationId=your.application.id +keystorePath=/full/path/to/your/keystore/file +keystorePwd=your_keystore_password +keystoreAlias=your_keystore_alias +keystoreAliasPwd=your_keystore_alias_password +``` + +These values will be picked up by the compilation process of the example app +and will set the application ID and signing configurations for you. + ## Authors & Collaborators -* [Adrián García](https://github.com/adriangl) - Author and maintainer -* [Daniel Sánchez Ceinos](https://github.com/danielceinos) - Contributor +* **[Adrián García](https://github.com/adriangl)** - *Author and maintainer* +* **[Daniel Sánchez Ceinos](https://github.com/danielceinos)** - *Contributor* ## License +This project is licensed under the Apache Software License, Version 2.0. ``` Copyright (C) 2019 BQ diff --git a/app/build.gradle b/app/build.gradle index 9620f0c..2517a6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,42 +21,99 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'com.gladed.androidgitversion' +apply from: "$rootDir/gradle/properties_utils.gradle" + +ext { + /* + ******************** + * Android variables + ******************** + */ + compile_sdk_version = 30 + min_sdk_version = 21 + target_sdk_version = 30 + build_tools_version = "30.0.2" +} + android { - compileSdkVersion project.ext.compileSdkVersion + compileSdkVersion compile_sdk_version + buildToolsVersion build_tools_version defaultConfig { - applicationId "com.bq.appupdateshelper" // Fake, replace with your own - minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion + /* + * Change your application ID to the app you want to test with + * + * Requires configuration via app_config.properties file or injected via environment variables. + * The loadEnvOrProperty will try to find the environment variable (in snake_case) or the + * property in signing.properties (converted to camelCase) + */ + applicationId loadEnvOrProperty("APPLICATION_ID", "/", "app_config.properties") + minSdkVersion min_sdk_version + targetSdkVersion target_sdk_version versionCode 1 versionName androidGitVersion.name() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + + buildFeatures { + viewBinding true + } + + signingConfigs { + /* + * Local release signing configuration + * + * Requires configuration via app_config.properties file or injected via environment variables. + * The loadEnvOrProperty will try to find the environment variable (in snake_case) or the + * property in signing.properties (converted to camelCase) + */ + localRelease { + storeFile file(loadEnvOrProperty("KEYSTORE_PATH", "/", "app_config.properties")) + storePassword loadEnvOrProperty("KEYSTORE_PWD", "", "app_config.properties") + keyAlias loadEnvOrProperty("KEYSTORE_ALIAS", "", "app_config.properties") + keyPassword loadEnvOrProperty("KEYSTORE_ALIAS_PWD", "", "app_config.properties") + } + } + buildTypes { + debug { + signingConfig signingConfigs.localRelease + } + release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.localRelease } } sourceSets.all { java.srcDirs += "src/${name}/kotlin" } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { implementation project(":lib") implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.core:core-ktx:1.0.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.constraintlayout:constraintlayout:2.0.2' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - implementation 'com.google.android.material:material:1.0.0' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation 'com.google.android.material:material:1.2.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 46626ac..6867204 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ + package="com.bq.appupdateshelper_app"> - + - + - + - + + + diff --git a/app/src/main/kotlin/com/bq/appupdateshelper/MainActivity.kt b/app/src/main/kotlin/com/bq/appupdateshelper_app/MainActivity.kt similarity index 59% rename from app/src/main/kotlin/com/bq/appupdateshelper/MainActivity.kt rename to app/src/main/kotlin/com/bq/appupdateshelper_app/MainActivity.kt index 7ad5ed2..d43897d 100644 --- a/app/src/main/kotlin/com/bq/appupdateshelper/MainActivity.kt +++ b/app/src/main/kotlin/com/bq/appupdateshelper_app/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 BQ + * Copyright (C) 2020 BQ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,28 +14,37 @@ * limitations under the License. */ -package com.bq.appupdateshelper +package com.bq.appupdateshelper_app import android.os.Bundle import android.widget.Button import androidx.appcompat.app.AppCompatActivity -import com.bq.appupdateshelper.fake.FakeUpdateActivity -import com.bq.appupdateshelper.flexible.FlexibleUpdateActivity -import com.bq.appupdateshelper.immediate.ImmediateUpdateActivity +import com.bq.appupdateshelper_app.databinding.MainActivityBinding +import com.bq.appupdateshelper_app.fake.FakeUpdateActivity +import com.bq.appupdateshelper_app.flexible.FlexibleUpdateActivity +import com.bq.appupdateshelper_app.fragment.FragmentUpdateActivity +import com.bq.appupdateshelper_app.immediate.ImmediateUpdateActivity class MainActivity : AppCompatActivity() { + private lateinit var binding: MainActivityBinding + private val immediateButton: Button - get() = findViewById(R.id.immediate_button) + get() = binding.immediateButton private val flexibleButton: Button - get() = findViewById(R.id.flexible_button) + get() = binding.flexibleButton + + private val fragmentButton: Button + get() = binding.fragmentButton private val fakeButton: Button - get() = findViewById(R.id.fake_button) + get() = binding.fakeButton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = MainActivityBinding.inflate(layoutInflater).apply { + setContentView(root) + } immediateButton.setOnClickListener { startActivity(ImmediateUpdateActivity.newIntent(this)) @@ -45,6 +54,10 @@ class MainActivity : AppCompatActivity() { startActivity(FlexibleUpdateActivity.newIntent(this)) } + fragmentButton.setOnClickListener { + startActivity(FragmentUpdateActivity.newIntent(this)) + } + fakeButton.setOnClickListener { startActivity(FakeUpdateActivity.newIntent(this)) } diff --git a/app/src/main/kotlin/com/bq/appupdateshelper/fake/FakeUpdateActivity.kt b/app/src/main/kotlin/com/bq/appupdateshelper_app/fake/FakeUpdateActivity.kt similarity index 82% rename from app/src/main/kotlin/com/bq/appupdateshelper/fake/FakeUpdateActivity.kt rename to app/src/main/kotlin/com/bq/appupdateshelper_app/fake/FakeUpdateActivity.kt index 3ccbfc5..02457eb 100644 --- a/app/src/main/kotlin/com/bq/appupdateshelper/fake/FakeUpdateActivity.kt +++ b/app/src/main/kotlin/com/bq/appupdateshelper_app/fake/FakeUpdateActivity.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.bq.appupdateshelper.fake +package com.bq.appupdateshelper_app.fake import android.content.Context import android.content.Intent @@ -26,8 +26,9 @@ import androidx.appcompat.app.AppCompatActivity import com.bq.appupdateshelper.AppUpdateInfoResult import com.bq.appupdateshelper.AppUpdateInstallState.Status.* import com.bq.appupdateshelper.FakeAppUpdatesHelper -import com.bq.appupdateshelper.R -import com.bq.appupdateshelper.misc.showToast +import com.bq.appupdateshelper_app.databinding.FakeUpdateActivityBinding +import com.bq.appupdateshelper_app.R +import com.bq.appupdateshelper_app.misc.showToast import com.google.android.material.snackbar.Snackbar /** @@ -48,12 +49,17 @@ class FakeUpdateActivity : AppCompatActivity() { private lateinit var fakeAppUpdatesHelper: FakeAppUpdatesHelper + private lateinit var binding: FakeUpdateActivityBinding + private val startUpdateButton: Button - get() = findViewById(R.id.start_update_button) + get() = binding.startUpdateButton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_fake_update) + + binding = FakeUpdateActivityBinding.inflate(layoutInflater).apply { + setContentView(root) + } setTitle(R.string.activity_fake_update_title) @@ -84,18 +90,18 @@ class FakeUpdateActivity : AppCompatActivity() { showToast("The update is downloading!") } DOWNLOADED -> { - showToast("The update has been downloaded!") + showToast("The update is downloading! Progress: ${installState.downloadProgress}") - Snackbar.make(findViewById(android.R.id.content), "Install the update?", Snackbar.LENGTH_INDEFINITE) - .apply { - setAction("Install") { - fakeAppUpdatesHelper.completeUpdate() + Snackbar.make(binding.root, "Install the update?", Snackbar.LENGTH_INDEFINITE) + .apply { + setAction("Install") { + fakeAppUpdatesHelper.completeUpdate() - // Fake installation process - fakeAppUpdatesHelper.completeFakeUpdate() + // Fake installation process + fakeAppUpdatesHelper.completeFakeUpdate() + } } - } - .show() + .show() } INSTALLING -> { showToast("The update is being installed!") @@ -106,13 +112,13 @@ class FakeUpdateActivity : AppCompatActivity() { FAILED -> { showToast("The update failed! Reason: ${installState.errorCode}") - Snackbar.make(findViewById(android.R.id.content), "Retry?", Snackbar.LENGTH_LONG) - .apply { - setAction("Retry") { - fakeAppUpdatesHelper.startImmediateUpdate(this@FakeUpdateActivity) + Snackbar.make(binding.root, "Retry?", Snackbar.LENGTH_LONG) + .apply { + setAction("Retry") { + fakeAppUpdatesHelper.startImmediateUpdate(this@FakeUpdateActivity) + } } - } - .show() + .show() } CANCELED -> { showToast("The user canceled the flexible update!") @@ -161,7 +167,7 @@ class FakeUpdateActivity : AppCompatActivity() { } } else { showToast("The update info could not be retrieved, " + - "cause: ${appUpdateInfoResult.exception!!.message}") + "cause: ${appUpdateInfoResult.exception!!.message}") } } } diff --git a/app/src/main/kotlin/com/bq/appupdateshelper/flexible/FlexibleUpdateActivity.kt b/app/src/main/kotlin/com/bq/appupdateshelper_app/flexible/FlexibleUpdateActivity.kt similarity index 84% rename from app/src/main/kotlin/com/bq/appupdateshelper/flexible/FlexibleUpdateActivity.kt rename to app/src/main/kotlin/com/bq/appupdateshelper_app/flexible/FlexibleUpdateActivity.kt index d85d41d..059283e 100644 --- a/app/src/main/kotlin/com/bq/appupdateshelper/flexible/FlexibleUpdateActivity.kt +++ b/app/src/main/kotlin/com/bq/appupdateshelper_app/flexible/FlexibleUpdateActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 BQ + * Copyright (C) 2020 BQ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.bq.appupdateshelper.flexible +package com.bq.appupdateshelper_app.flexible import android.content.Context import android.content.Intent @@ -25,8 +25,9 @@ import androidx.appcompat.app.AppCompatActivity import com.bq.appupdateshelper.AppUpdateInfoResult import com.bq.appupdateshelper.AppUpdateInstallState.Status.* import com.bq.appupdateshelper.AppUpdatesHelper -import com.bq.appupdateshelper.R -import com.bq.appupdateshelper.misc.showToast +import com.bq.appupdateshelper_app.databinding.FlexibleUpdateActivityBinding +import com.bq.appupdateshelper_app.R +import com.bq.appupdateshelper_app.misc.showToast import com.google.android.material.snackbar.Snackbar /** @@ -44,12 +45,16 @@ class FlexibleUpdateActivity : AppCompatActivity() { private lateinit var appUpdatesHelper: AppUpdatesHelper + private lateinit var binding: FlexibleUpdateActivityBinding + private val startUpdateButton: Button - get() = findViewById(R.id.start_update_button) + get() = binding.startUpdateButton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_flexible_update) + binding = FlexibleUpdateActivityBinding.inflate(layoutInflater).apply { + setContentView(root) + } setTitle(R.string.activity_flexible_update_title) @@ -83,19 +88,19 @@ class FlexibleUpdateActivity : AppCompatActivity() { showToast("Waiting for update to start!") } DOWNLOADING -> { - showToast("The update is downloading!") + showToast("The update is downloading! Progress: ${installState.downloadProgress}") } DOWNLOADED -> { showToast("The update has been downloaded!") // Prompt the user to install the update when we know that the update has been // downloaded successfully - Snackbar.make(findViewById(android.R.id.content), "Install the update?", Snackbar.LENGTH_INDEFINITE) - .apply { - setAction("Install") { - appUpdatesHelper.completeUpdate() + Snackbar.make(binding.root, "Install the update?", Snackbar.LENGTH_INDEFINITE) + .apply { + setAction("Install") { + appUpdatesHelper.completeUpdate() + } } - } - .show() + .show() } INSTALLING -> { showToast("The update is being installed!") @@ -111,13 +116,13 @@ class FlexibleUpdateActivity : AppCompatActivity() { // process if needed showToast("The update failed! Reason: ${installState.errorCode}") - Snackbar.make(findViewById(android.R.id.content), "Retry?", Snackbar.LENGTH_LONG) - .apply { - setAction("Retry") { - appUpdatesHelper.startFlexibleUpdate(this@FlexibleUpdateActivity) + Snackbar.make(binding.root, "Retry?", Snackbar.LENGTH_LONG) + .apply { + setAction("Retry") { + appUpdatesHelper.startFlexibleUpdate(this@FlexibleUpdateActivity) + } } - } - .show() + .show() } CANCELED -> { // This state is only reachable in flexible updates and it happens when the @@ -167,7 +172,7 @@ class FlexibleUpdateActivity : AppCompatActivity() { } } else { showToast("The update info could not be retrieved, " + - "cause: ${appUpdateInfoResult.exception!!.message}") + "cause: ${appUpdateInfoResult.exception!!.message}") } } } diff --git a/app/src/main/kotlin/com/bq/appupdateshelper_app/fragment/FragmentUpdateActivity.kt b/app/src/main/kotlin/com/bq/appupdateshelper_app/fragment/FragmentUpdateActivity.kt new file mode 100644 index 0000000..b51f914 --- /dev/null +++ b/app/src/main/kotlin/com/bq/appupdateshelper_app/fragment/FragmentUpdateActivity.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 BQ + * + * 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 com.bq.appupdateshelper_app.fragment + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.bq.appupdateshelper.AppUpdatesHelper +import com.bq.appupdateshelper_app.R +import com.bq.appupdateshelper_app.databinding.FragmentUpdateActivityBinding + +/** + * Activity that illustrates the use of the [AppUpdatesHelper] class of the lib to start flexible + * updates in a fragment. + */ +class FragmentUpdateActivity : AppCompatActivity() { + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, FragmentUpdateActivity::class.java) + } + } + + private lateinit var binding: FragmentUpdateActivityBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = FragmentUpdateActivityBinding.inflate(layoutInflater).apply { + setContentView(root) + } + + setTitle(R.string.activity_fragment_update_title) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/bq/appupdateshelper_app/fragment/FragmentUpdateFragment.kt b/app/src/main/kotlin/com/bq/appupdateshelper_app/fragment/FragmentUpdateFragment.kt new file mode 100644 index 0000000..c8db2eb --- /dev/null +++ b/app/src/main/kotlin/com/bq/appupdateshelper_app/fragment/FragmentUpdateFragment.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2020 BQ + * + * 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 com.bq.appupdateshelper_app.fragment + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.fragment.app.Fragment +import com.bq.appupdateshelper.AppUpdateInfoResult +import com.bq.appupdateshelper.AppUpdateInstallState +import com.bq.appupdateshelper.AppUpdatesHelper +import com.bq.appupdateshelper_app.databinding.FragmentUpdateFragmentBinding +import com.bq.appupdateshelper_app.misc.showToast +import com.google.android.material.snackbar.Snackbar + +/** + * Fragment that illustrates the use of the [AppUpdatesHelper] class of the lib to start flexible + * updates in a fragment. + */ +class FragmentUpdateFragment : Fragment() { + companion object { + private const val TAG = "FragmentUpdateFragment" + + fun newInstance(): FragmentUpdateFragment { + val args = Bundle() + + val fragment = FragmentUpdateFragment() + fragment.arguments = args + return fragment + } + } + + private lateinit var appUpdatesHelper: AppUpdatesHelper + + private lateinit var binding: FragmentUpdateFragmentBinding + + private val startUpdateButton: Button + get() = binding.startUpdateButton + + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View? = + FragmentUpdateFragmentBinding.inflate(inflater, container, false) + .also { binding = it }.root + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + // Instantiate the app updates helper and start using it with startListening() + appUpdatesHelper = AppUpdatesHelper(requireContext()) + + appUpdatesHelper.startListening { installState -> + // The update process is tracked here from the moment the user clicks "Update" until the + // app is fully installed + Log.d(TAG, "Update install state: $installState") + + when (installState.status) { + AppUpdateInstallState.Status.UNKNOWN -> { + showToast("Unknown install state") + } + AppUpdateInstallState.Status.DENIED -> { + // This should only be reached in immediate updates + showToast("The user denied the flexible update!") + requireActivity().finish() + } + AppUpdateInstallState.Status.REQUIRES_UI_INTENT -> { + // The docs don't really say anything about this state or when it's triggered + // so... + showToast("The update needs an UI intent!") + } + AppUpdateInstallState.Status.UPDATE_ACCEPTED -> { + // The user has accepted the update, so you can notify the user + showToast("The user accepted the update!") + } + AppUpdateInstallState.Status.PENDING -> { + showToast("Waiting for update to start!") + } + AppUpdateInstallState.Status.DOWNLOADING -> { + showToast("The update is downloading! Progress: ${installState.downloadProgress}") + } + AppUpdateInstallState.Status.DOWNLOADED -> { + showToast("The update has been downloaded!") + // Prompt the user to install the update when we know that the update has been + // downloaded successfully + Snackbar.make(binding.root, "Install the update?", Snackbar.LENGTH_INDEFINITE) + .apply { + setAction("Install") { + appUpdatesHelper.completeUpdate() + } + } + .show() + } + AppUpdateInstallState.Status.INSTALLING -> { + showToast("The update is being installed!") + } + AppUpdateInstallState.Status.INSTALLED -> { + // Usually you won't get up to this state, since the app closes automatically + // in the installation process. Anyway, it's not a bad practice to consider this + // case + showToast("The update has been installed!") + } + AppUpdateInstallState.Status.FAILED -> { + // The installation failed for some reason, here you can retry the update + // process if needed + showToast("The update failed! Reason: ${installState.errorCode}") + + Snackbar.make(binding.root, "Retry?", Snackbar.LENGTH_LONG) + .apply { + setAction("Retry") { + appUpdatesHelper.startFlexibleUpdate(this@FragmentUpdateFragment) + } + } + .show() + } + AppUpdateInstallState.Status.CANCELED -> { + // This state is only reachable in flexible updates and it happens when the + // user cancels a flexible update. There's no need to do anything in this case. + showToast("The user canceled the flexible update!") + } + } + } + + startUpdateButton.setOnClickListener { + // Start the update flow by first checking if there's any update available for the app + // in the Play Store using getAppUpdateInfo() + appUpdatesHelper.getAppUpdateInfo { appUpdateInfoResult -> + Log.d(TAG, "App update info: $appUpdateInfoResult") + + if (appUpdateInfoResult.isSuccessful) { + // If the request went well, you can check the state of the app update + when (appUpdateInfoResult.updateAvailability!!) { + AppUpdateInfoResult.Availability.UNKNOWN -> { + showToast("The state of the update is unknown!") + } + AppUpdateInfoResult.Availability.UPDATE_NOT_AVAILABLE -> { + showToast("No update available!") + } + AppUpdateInfoResult.Availability.UPDATE_AVAILABLE -> { + showToast("Update available!") + // When we know that the update is available, we must check if we can + // perform the desired type of update + if (appUpdateInfoResult.canInstallFlexibleUpdate()) { + // Start the update flow + showToast("Can install flexible update!") + appUpdatesHelper.startFlexibleUpdate(this) + } else { + showToast("Can not install flexible update!") + } + } + AppUpdateInfoResult.Availability.UPDATE_IN_PROGRESS -> { + // If the update is in progress, we don't need to jump to the flexible flow again + showToast("Update in progress!") + } + AppUpdateInfoResult.Availability.UPDATE_DOWNLOADED -> { + // The app update is downloaded, but the install flow has not started + // yet, so complete the update + showToast("Update downloaded!") + appUpdatesHelper.completeUpdate() + } + } + } else { + showToast("The update info could not be retrieved, " + + "cause: ${appUpdateInfoResult.exception!!.message}") + } + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + // We have to bind the helper to the activity results so it can properly dispatch some + // update events + appUpdatesHelper.onUpdateStatusResult(requestCode, resultCode) + } + + override fun onDestroyView() { + // Stop listening for updates with stopListening() + super.onDestroyView() + appUpdatesHelper.stopListening() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/bq/appupdateshelper/immediate/ImmediateUpdateActivity.kt b/app/src/main/kotlin/com/bq/appupdateshelper_app/immediate/ImmediateUpdateActivity.kt similarity index 88% rename from app/src/main/kotlin/com/bq/appupdateshelper/immediate/ImmediateUpdateActivity.kt rename to app/src/main/kotlin/com/bq/appupdateshelper_app/immediate/ImmediateUpdateActivity.kt index 1badca6..c1ecf41 100644 --- a/app/src/main/kotlin/com/bq/appupdateshelper/immediate/ImmediateUpdateActivity.kt +++ b/app/src/main/kotlin/com/bq/appupdateshelper_app/immediate/ImmediateUpdateActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 BQ + * Copyright (C) 2020 BQ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.bq.appupdateshelper.immediate +package com.bq.appupdateshelper_app.immediate import android.content.Context import android.content.Intent @@ -25,8 +25,9 @@ import androidx.appcompat.app.AppCompatActivity import com.bq.appupdateshelper.AppUpdateInfoResult import com.bq.appupdateshelper.AppUpdateInstallState.Status.* import com.bq.appupdateshelper.AppUpdatesHelper -import com.bq.appupdateshelper.R -import com.bq.appupdateshelper.misc.showToast +import com.bq.appupdateshelper_app.databinding.ImmediateUpdateActivityBinding +import com.bq.appupdateshelper_app.R +import com.bq.appupdateshelper_app.misc.showToast import com.google.android.material.snackbar.Snackbar /** @@ -44,12 +45,16 @@ class ImmediateUpdateActivity : AppCompatActivity() { private lateinit var appUpdatesHelper: AppUpdatesHelper + private lateinit var binding: ImmediateUpdateActivityBinding + private val startUpdateButton: Button - get() = findViewById(R.id.start_update_button) + get() = binding.startUpdateButton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_immediate_update) + binding = ImmediateUpdateActivityBinding.inflate(layoutInflater).apply { + setContentView(root) + } setTitle(R.string.activity_immediate_update_title) @@ -85,7 +90,7 @@ class ImmediateUpdateActivity : AppCompatActivity() { showToast("Waiting for update to start!") } DOWNLOADING -> { - showToast("The update is downloading!") + showToast("The update is downloading! Progress: ${installState.downloadProgress}") } DOWNLOADED -> { showToast("The update has been downloaded!") @@ -104,13 +109,13 @@ class ImmediateUpdateActivity : AppCompatActivity() { // process if needed showToast("The update failed! Reason: ${installState.errorCode}") - Snackbar.make(findViewById(android.R.id.content), "Retry?", Snackbar.LENGTH_LONG) - .apply { - setAction("Retry") { - appUpdatesHelper.startImmediateUpdate(this@ImmediateUpdateActivity) + Snackbar.make(binding.root, "Retry?", Snackbar.LENGTH_LONG) + .apply { + setAction("Retry") { + appUpdatesHelper.startImmediateUpdate(this@ImmediateUpdateActivity) + } } - } - .show() + .show() } CANCELED -> { // This state is only reachable in flexible updates. @@ -160,7 +165,7 @@ class ImmediateUpdateActivity : AppCompatActivity() { } } else { showToast("The update info could not be retrieved, " + - "cause: ${appUpdateInfoResult.exception!!.message}") + "cause: ${appUpdateInfoResult.exception!!.message}") } } } diff --git a/app/src/main/kotlin/com/bq/appupdateshelper/misc/ActivityExtensions.kt b/app/src/main/kotlin/com/bq/appupdateshelper_app/misc/ActivityExtensions.kt similarity index 97% rename from app/src/main/kotlin/com/bq/appupdateshelper/misc/ActivityExtensions.kt rename to app/src/main/kotlin/com/bq/appupdateshelper_app/misc/ActivityExtensions.kt index 6ffac4e..5923ba5 100644 --- a/app/src/main/kotlin/com/bq/appupdateshelper/misc/ActivityExtensions.kt +++ b/app/src/main/kotlin/com/bq/appupdateshelper_app/misc/ActivityExtensions.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.bq.appupdateshelper.misc +package com.bq.appupdateshelper_app.misc import android.app.Activity import android.widget.Toast diff --git a/app/src/main/kotlin/com/bq/appupdateshelper_app/misc/FragmentExtensions.kt b/app/src/main/kotlin/com/bq/appupdateshelper_app/misc/FragmentExtensions.kt new file mode 100644 index 0000000..c1f30d2 --- /dev/null +++ b/app/src/main/kotlin/com/bq/appupdateshelper_app/misc/FragmentExtensions.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 BQ + * + * 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 com.bq.appupdateshelper_app.misc + +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment + +/** + * Displays a text as a toast in the current fragment. + * + * @param text Text to display in the toast + * @param duration Duration, one of [Toast.LENGTH_SHORT] or [Toast.LENGTH_LONG] + */ +fun Fragment.showToast(text: String, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(context, text, duration).show() +} + +/** + * Displays a text as a toast in the current fragment. + * + * @param stringResId Text to display in the toast as a string resource ID + * @param duration Duration, one of [Toast.LENGTH_SHORT] or [Toast.LENGTH_LONG] + */ +fun Fragment.showToast(@StringRes stringResId: Int, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(context, stringResId, duration).show() +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_fake_update.xml b/app/src/main/res/layout/fake_update_activity.xml similarity index 96% rename from app/src/main/res/layout/activity_fake_update.xml rename to app/src/main/res/layout/fake_update_activity.xml index eb85412..a1eda58 100644 --- a/app/src/main/res/layout/activity_fake_update.xml +++ b/app/src/main/res/layout/fake_update_activity.xml @@ -21,7 +21,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_height="match_parent" android:layout_width="match_parent" - tools:context="com.bq.appupdateshelper.fake.FakeUpdateActivity" + tools:context="com.bq.appupdateshelper_app.fake.FakeUpdateActivity" tools:ignore="HardcodedText"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_update_fragment.xml b/app/src/main/res/layout/fragment_update_fragment.xml new file mode 100644 index 0000000..3853a98 --- /dev/null +++ b/app/src/main/res/layout/fragment_update_fragment.xml @@ -0,0 +1,54 @@ + + + + + + + + + +