Compare commits

..

No commits in common. "patch-develop" and "v2.3.3-patch4" have entirely different histories.

38 changed files with 773 additions and 86 deletions

2
.idea/gradle.xml generated
View File

@ -7,13 +7,13 @@
<option name="testRunner" value="GRADLE" /> <option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Embedded JDK" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

2
.idea/misc.xml generated
View File

@ -61,7 +61,7 @@
</profile-state> </profile-state>
</entry> </entry>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

3
.idea/modules.xml generated
View File

@ -4,9 +4,6 @@
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/Another_Widget.iml" filepath="$PROJECT_DIR$/.idea/modules/Another_Widget.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/Another_Widget.iml" filepath="$PROJECT_DIR$/.idea/modules/Another_Widget.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.main.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Another_Widget.app.unitTest.iml" />
</modules> </modules>
</component> </component>
</project> </project>

View File

@ -1,8 +1,16 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
// Apply the Crashlytics Gradle plugin
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
def apikeyPropertiesFile = rootProject.file("apikey.properties")
def apikeyProperties = new Properties()
apikeyProperties.load(new FileInputStream(apikeyPropertiesFile))
android { android {
compileSdkVersion 30 compileSdkVersion 30
@ -15,6 +23,7 @@ android {
versionName "2.3.3" versionName "2.3.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "GOOGLE_API_KEY", apikeyProperties['GOOGLE_API_KEY'])
renderscriptSupportModeEnabled true renderscriptSupportModeEnabled true
} }
@ -94,8 +103,17 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.12.0' implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0' kapt 'com.github.bumptech.glide:compiler:4.11.0'
// Fitness
implementation 'com.google.android.gms:play-services-fitness:20.0.0'
implementation 'com.google.android.gms:play-services-auth:19.0.0'
//Weather //Weather
implementation 'com.github.KwabenBerko:OpenWeatherMap-Android-Library:2.0.2' implementation 'com.github.KwabenBerko:OpenWeatherMap-Android-Library:2.0.2'
implementation 'com.google.android.gms:play-services-location:18.0.0'
// Billing
implementation 'com.android.billingclient:billing:3.0.3'
implementation 'com.android.billingclient:billing-ktx:3.0.3'
// KTX // KTX
implementation "androidx.core:core-ktx:1.5.0" implementation "androidx.core:core-ktx:1.5.0"
@ -113,6 +131,9 @@ dependencies {
//Coroutines //Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
// Add the Firebase SDK for Crashlytics.
implementation 'com.google.firebase:firebase-crashlytics:18.0.0'
// Preferences // Preferences
implementation 'com.chibatching.kotpref:kotpref:2.13.1' implementation 'com.chibatching.kotpref:kotpref:2.13.1'
implementation 'com.chibatching.kotpref:livedata-support:2.13.1' implementation 'com.chibatching.kotpref:livedata-support:2.13.1'

View File

@ -37,6 +37,7 @@
<activity android:name=".ui.activities.tabs.CustomFontActivity" android:screenOrientation="portrait" /> <activity android:name=".ui.activities.tabs.CustomFontActivity" android:screenOrientation="portrait" />
<activity android:name=".ui.activities.tabs.CustomLocationActivity" android:screenOrientation="portrait" /> <activity android:name=".ui.activities.tabs.CustomLocationActivity" android:screenOrientation="portrait" />
<activity android:name=".ui.activities.tabs.WeatherProviderActivity" android:screenOrientation="portrait" /> <activity android:name=".ui.activities.tabs.WeatherProviderActivity" android:screenOrientation="portrait" />
<activity android:name=".ui.activities.settings.SupportDevActivity" android:screenOrientation="portrait" />
<activity android:name=".ui.activities.tabs.CustomDateActivity" android:screenOrientation="portrait" /> <activity android:name=".ui.activities.tabs.CustomDateActivity" android:screenOrientation="portrait" />
<activity android:name=".ui.activities.settings.IntegrationsActivity" android:screenOrientation="portrait" /> <activity android:name=".ui.activities.settings.IntegrationsActivity" android:screenOrientation="portrait" />
<activity android:name=".ui.activities.tabs.MusicPlayersFilterActivity" android:screenOrientation="portrait" /> <activity android:name=".ui.activities.tabs.MusicPlayersFilterActivity" android:screenOrientation="portrait" />
@ -125,6 +126,16 @@
<action android:name="android.intent.action.BATTERY_CHANGED"/> <action android:name="android.intent.action.BATTERY_CHANGED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".receivers.ActivityDetectionReceiver"
android:exported="true"
android:permission="com.google.android.gms.permission.ACTIVITY_RECOGNITION">
<intent-filter>
<action android:name="com.mypackage.ACTION_PROCESS_ACTIVITY_TRANSITIONS" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application> </application>
<queries> <queries>

View File

@ -1,14 +1,20 @@
package com.tommasoberlose.anotherwidget package com.tommasoberlose.anotherwidget
import android.Manifest
import android.app.Application import android.app.Application
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.chibatching.kotpref.Kotpref import com.chibatching.kotpref.Kotpref
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tommasoberlose.anotherwidget.global.Preferences import com.tommasoberlose.anotherwidget.global.Preferences
class AWApplication : Application() { class AWApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Firebase crashlitycs
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
// Preferences // Preferences
Kotpref.init(this) Kotpref.init(this)

View File

@ -6,9 +6,18 @@ import android.app.AlarmManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.tommasoberlose.anotherwidget.R import com.tommasoberlose.anotherwidget.R
import com.tommasoberlose.anotherwidget.databinding.GlanceProviderSettingsLayoutBinding import com.tommasoberlose.anotherwidget.databinding.GlanceProviderSettingsLayoutBinding
import com.tommasoberlose.anotherwidget.global.Constants import com.tommasoberlose.anotherwidget.global.Constants
@ -17,6 +26,7 @@ import com.tommasoberlose.anotherwidget.helpers.ActiveNotificationsHelper
import com.tommasoberlose.anotherwidget.helpers.AlarmHelper import com.tommasoberlose.anotherwidget.helpers.AlarmHelper
import com.tommasoberlose.anotherwidget.helpers.GreetingsHelper import com.tommasoberlose.anotherwidget.helpers.GreetingsHelper
import com.tommasoberlose.anotherwidget.helpers.MediaPlayerHelper import com.tommasoberlose.anotherwidget.helpers.MediaPlayerHelper
import com.tommasoberlose.anotherwidget.receivers.ActivityDetectionReceiver
import com.tommasoberlose.anotherwidget.ui.activities.tabs.AppNotificationsFilterActivity import com.tommasoberlose.anotherwidget.ui.activities.tabs.AppNotificationsFilterActivity
import com.tommasoberlose.anotherwidget.ui.activities.tabs.MediaInfoFormatActivity import com.tommasoberlose.anotherwidget.ui.activities.tabs.MediaInfoFormatActivity
import com.tommasoberlose.anotherwidget.ui.activities.tabs.MusicPlayersFilterActivity import com.tommasoberlose.anotherwidget.ui.activities.tabs.MusicPlayersFilterActivity
@ -80,6 +90,14 @@ class GlanceSettingsDialog(val context: Activity, val provider: Constants.Glance
checkNextAlarm() checkNextAlarm()
} }
/* GOOGLE STEPS */
binding.actionToggleGoogleFit.isVisible = provider == Constants.GlanceProviderId.GOOGLE_FIT_STEPS
if (provider == Constants.GlanceProviderId.GOOGLE_FIT_STEPS) {
binding.warningContainer.isVisible = false
checkFitnessPermission()
checkGoogleFitConnection()
}
/* BATTERY INFO */ /* BATTERY INFO */
if (provider == Constants.GlanceProviderId.BATTERY_LEVEL_LOW) { if (provider == Constants.GlanceProviderId.BATTERY_LEVEL_LOW) {
binding.warningContainer.isVisible = false binding.warningContainer.isVisible = false
@ -175,14 +193,34 @@ class GlanceSettingsDialog(val context: Activity, val provider: Constants.Glance
Preferences.showGreetings = isChecked Preferences.showGreetings = isChecked
GreetingsHelper.toggleGreetings(context) GreetingsHelper.toggleGreetings(context)
} }
Constants.GlanceProviderId.GOOGLE_FIT_STEPS -> {
if (isChecked) {
val account: GoogleSignInAccount? =
GoogleSignIn.getLastSignedInAccount(context)
if (!GoogleSignIn.hasPermissions(account,
ActivityDetectionReceiver.FITNESS_OPTIONS
)
) {
val mGoogleSignInClient =
GoogleSignIn.getClient(context, GoogleSignInOptions.Builder(
GoogleSignInOptions.DEFAULT_SIGN_IN).addExtension(
ActivityDetectionReceiver.FITNESS_OPTIONS
).build())
context.startActivityForResult(mGoogleSignInClient.signInIntent,
2)
} else {
Preferences.showDailySteps = true
}
} else {
Preferences.showDailySteps = false
}
binding.warningContainer.isVisible = false
checkFitnessPermission()
checkGoogleFitConnection()
}
Constants.GlanceProviderId.EVENTS -> { Constants.GlanceProviderId.EVENTS -> {
Preferences.showEventsAsGlanceProvider = isChecked Preferences.showEventsAsGlanceProvider = isChecked
if (isChecked) {
com.tommasoberlose.anotherwidget.db.EventRepository(context).run {
resetNextEventData()
close()
}
}
} }
Constants.GlanceProviderId.WEATHER -> { Constants.GlanceProviderId.WEATHER -> {
Preferences.showWeatherAsGlanceProvider = isChecked Preferences.showWeatherAsGlanceProvider = isChecked
@ -289,4 +327,89 @@ class GlanceSettingsDialog(val context: Activity, val provider: Constants.Glance
} }
statusCallback?.invoke() statusCallback?.invoke()
} }
private fun checkFitnessPermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || context.checkGrantedPermission(
Manifest.permission.ACTIVITY_RECOGNITION)
) {
if (Preferences.showDailySteps) {
ActivityDetectionReceiver.registerFence(context)
} else {
ActivityDetectionReceiver.unregisterFence(context)
}
} else if (Preferences.showDailySteps) {
ActivityDetectionReceiver.unregisterFence(context)
binding.warningContainer.isVisible = true
binding.warningTitle.text = context.getString(R.string.settings_request_fitness_access)
binding.warningContainer.setOnClickListener {
requireFitnessPermission()
}
} else {
ActivityDetectionReceiver.unregisterFence(context)
}
statusCallback?.invoke()
}
private fun checkGoogleFitConnection() {
val account: GoogleSignInAccount? = GoogleSignIn.getLastSignedInAccount(context)
if (!GoogleSignIn.hasPermissions(account,
ActivityDetectionReceiver.FITNESS_OPTIONS
)) {
binding.warningContainer.isVisible = true
binding.warningTitle.text = context.getString(R.string.settings_request_fitness_access)
binding.warningContainer.setOnClickListener {
GoogleSignIn.requestPermissions(
context,
1,
account,
ActivityDetectionReceiver.FITNESS_OPTIONS)
}
binding.actionConnectToGoogleFit.isVisible = true
binding.actionDisconnectToGoogleFit.isVisible = false
binding.actionConnectToGoogleFit.setOnClickListener {
GoogleSignIn.requestPermissions(
context,
1,
account,
ActivityDetectionReceiver.FITNESS_OPTIONS)
}
binding.actionDisconnectToGoogleFit.setOnClickListener(null)
binding.googleFitStatusLabel.text = context.getString(R.string.google_fit_account_not_connected)
} else {
binding.actionConnectToGoogleFit.isVisible = false
binding.actionDisconnectToGoogleFit.isVisible = true
binding.actionConnectToGoogleFit.setOnClickListener(null)
binding.actionDisconnectToGoogleFit.setOnClickListener {
GoogleSignIn.getClient(context, GoogleSignInOptions.Builder(
GoogleSignInOptions.DEFAULT_SIGN_IN).addExtension(
ActivityDetectionReceiver.FITNESS_OPTIONS
).build()).signOut().addOnCompleteListener {
show()
}
}
binding.googleFitStatusLabel.text = context.getString(R.string.google_fit_account_connected)
}
}
private fun requireFitnessPermission() {
Dexter.withContext(context)
.withPermissions(
"com.google.android.gms.permission.ACTIVITY_RECOGNITION",
"android.gms.permission.ACTIVITY_RECOGNITION",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Manifest.permission.ACTIVITY_RECOGNITION else "com.google.android.gms.permission.ACTIVITY_RECOGNITION"
).withListener(object: MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
checkFitnessPermission()
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?,
token: PermissionToken?
) {
// Remember to invoke this method when the custom rationale is closed
// or just by default if you don't want to use any custom rationale.
token?.continuePermissionRequest()
}
})
.check()
}
} }

View File

@ -27,7 +27,7 @@ class EventRepository(val context: Context) {
db.runInTransaction{ db.runInTransaction{
db.dao().run { db.dao().run {
deleteAll() deleteAll()
insert(eventList) insertAll(eventList)
} }
} }
} }
@ -39,6 +39,12 @@ class EventRepository(val context: Context) {
fun resetNextEventData() { fun resetNextEventData() {
Preferences.bulk { Preferences.bulk {
remove(Preferences::nextEventId) remove(Preferences::nextEventId)
remove(Preferences::nextEventName)
remove(Preferences::nextEventStartDate)
remove(Preferences::nextEventAllDay)
remove(Preferences::nextEventLocation)
remove(Preferences::nextEventEndDate)
remove(Preferences::nextEventCalendarId)
} }
} }
@ -69,7 +75,7 @@ class EventRepository(val context: Context) {
val events = getEvents() val events = getEvents()
if (events.isNotEmpty()) { if (events.isNotEmpty()) {
val newNextEvent = events.first() val newNextEvent = events.first()
saveNextEventData(newNextEvent) Preferences.nextEventId = newNextEvent.id
newNextEvent newNextEvent
} else { } else {
resetNextEventData() resetNextEventData()
@ -87,9 +93,9 @@ class EventRepository(val context: Context) {
if (eventList.isNotEmpty()) { if (eventList.isNotEmpty()) {
val index = eventList.indexOfFirst { it.id == Preferences.nextEventId } val index = eventList.indexOfFirst { it.id == Preferences.nextEventId }
if (index > -1 && index < eventList.size - 1) { if (index > -1 && index < eventList.size - 1) {
saveNextEventData(eventList[index + 1]) Preferences.nextEventId = eventList[index + 1].id
} else { } else {
saveNextEventData(eventList.first()) Preferences.nextEventId = eventList.first().id
} }
} else { } else {
resetNextEventData() resetNextEventData()
@ -105,9 +111,9 @@ class EventRepository(val context: Context) {
if (eventList.isNotEmpty()) { if (eventList.isNotEmpty()) {
val index = eventList.indexOfFirst { it.id == Preferences.nextEventId } val index = eventList.indexOfFirst { it.id == Preferences.nextEventId }
if (index > 0) { if (index > 0) {
saveNextEventData(eventList[index - 1]) Preferences.nextEventId = eventList[index - 1].id
} else { } else {
saveNextEventData(eventList.last()) Preferences.nextEventId = eventList.last().id
} }
} else { } else {
resetNextEventData() resetNextEventData()
@ -119,7 +125,7 @@ class EventRepository(val context: Context) {
} }
fun getFutureEvents(): List<Event> { fun getFutureEvents(): List<Event> {
return db.dao().find(Calendar.getInstance().timeInMillis).sortEvents() return db.dao().findFuture(Calendar.getInstance().timeInMillis).applyFilters().sortEvents()
} }
private fun getEvents(): List<Event> { private fun getEvents(): List<Event> {
@ -138,7 +144,7 @@ class EventRepository(val context: Context) {
else -> add(Calendar.HOUR, 6) else -> add(Calendar.HOUR, 6)
} }
} }
return db.dao().find(now, limit.timeInMillis).sortEvents() return db.dao().find(now, limit.timeInMillis).applyFilters().sortEvents()
} }
fun getEventsCount(): Int = getEvents().size fun getEventsCount(): Int = getEvents().size
@ -150,16 +156,16 @@ class EventRepository(val context: Context) {
@Dao @Dao
interface EventDao { interface EventDao {
@Query("SELECT * FROM events WHERE id = :id LIMIT 1") @Query("SELECT * FROM events WHERE id = :id LIMIT 1")
fun findById(id: Long): Event? fun findById(id: Long) : Event?
@Query("SELECT * FROM events WHERE end_date > :from") @Query("SELECT * FROM events WHERE end_date > :from")
fun find(from: Long): List<Event> fun findFuture(from: Long) : List<Event>
@Query("SELECT * FROM events WHERE end_date > :from AND start_date <= :to") @Query("SELECT * FROM events WHERE end_date > :from and start_date <= :to")
fun find(from: Long, to: Long): List<Event> fun find(from: Long, to: Long) : List<Event>
@Insert @Insert
fun insert(events: List<Event>) fun insertAll(events: List<Event>)
@Query("DELETE FROM events") @Query("DELETE FROM events")
fun deleteAll() fun deleteAll()

View File

@ -10,6 +10,7 @@ object Actions {
const val ACTION_OPEN_WEATHER_INTENT = "com.tommasoberlose.anotherwidget.action.OPEN_WEATHER_INTENT" const val ACTION_OPEN_WEATHER_INTENT = "com.tommasoberlose.anotherwidget.action.OPEN_WEATHER_INTENT"
const val ACTION_GO_TO_NEXT_EVENT = "com.tommasoberlose.anotherwidget.action.GO_TO_NEXT_EVENT" const val ACTION_GO_TO_NEXT_EVENT = "com.tommasoberlose.anotherwidget.action.GO_TO_NEXT_EVENT"
const val ACTION_GO_TO_PREVIOUS_EVENT = "com.tommasoberlose.anotherwidget.action.GO_TO_PREVIOUS_EVENT" const val ACTION_GO_TO_PREVIOUS_EVENT = "com.tommasoberlose.anotherwidget.action.GO_TO_PREVIOUS_EVENT"
const val ACTION_REPORT_CRASH = "com.tommasoberlose.anotherwidget.action.REPORT_CRASH"
const val ACTION_CLEAR_NOTIFICATION = "com.tommasoberlose.anotherwidget.action.CLEAR_NOTIFICATION" const val ACTION_CLEAR_NOTIFICATION = "com.tommasoberlose.anotherwidget.action.CLEAR_NOTIFICATION"
const val ACTION_UPDATE_GREETINGS = "com.tommasoberlose.anotherwidget.action.UPDATE_GREETINGS" const val ACTION_UPDATE_GREETINGS = "com.tommasoberlose.anotherwidget.action.UPDATE_GREETINGS"

View File

@ -23,6 +23,12 @@ object Preferences : KotprefModel() {
var calendarFilter by stringPref(key = "PREF_CALENDAR_FILTER", default = "") var calendarFilter by stringPref(key = "PREF_CALENDAR_FILTER", default = "")
var nextEventId by longPref(key = "PREF_NEXT_EVENT_ID", default = -1) var nextEventId by longPref(key = "PREF_NEXT_EVENT_ID", default = -1)
var nextEventName by stringPref(key = "PREF_NEXT_EVENT_NAME")
var nextEventStartDate by longPref(key = "PREF_NEXT_EVENT_START_DATE")
var nextEventAllDay by booleanPref(key = "PREF_NEXT_EVENT_ALL_DAY")
var nextEventLocation by stringPref(key = "PREF_NEXT_EVENT_LOCATION")
var nextEventEndDate by longPref(key = "PREF_NEXT_EVENT_END_DATE")
var nextEventCalendarId by intPref(key = "PREF_NEXT_EVENT_CALENDAR_ID")
var customLocationLat by stringPref(key = "PREF_CUSTOM_LOCATION_LAT", default = "") var customLocationLat by stringPref(key = "PREF_CUSTOM_LOCATION_LAT", default = "")
var customLocationLon by stringPref(key = "PREF_CUSTOM_LOCATION_LON", default = "") var customLocationLon by stringPref(key = "PREF_CUSTOM_LOCATION_LON", default = "")
var customLocationAdd by stringPref(key = "PREF_CUSTOM_LOCATION_ADD", default = "") var customLocationAdd by stringPref(key = "PREF_CUSTOM_LOCATION_ADD", default = "")

View File

@ -5,7 +5,9 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.Log
import com.tommasoberlose.anotherwidget.global.Actions import com.tommasoberlose.anotherwidget.global.Actions
import com.tommasoberlose.anotherwidget.receivers.ActivityDetectionReceiver
import com.tommasoberlose.anotherwidget.receivers.UpdatesReceiver import com.tommasoberlose.anotherwidget.receivers.UpdatesReceiver
import com.tommasoberlose.anotherwidget.utils.setExactIfCanSchedule import com.tommasoberlose.anotherwidget.utils.setExactIfCanSchedule
import java.text.SimpleDateFormat import java.text.SimpleDateFormat

View File

@ -4,9 +4,11 @@ import android.content.Context
import android.graphics.* import android.graphics.*
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
object BitmapHelper { object BitmapHelper {
@ -37,6 +39,15 @@ object BitmapHelper {
1 1
} }
if (draw) {
FirebaseCrashlytics.getInstance().setCustomKey("WIDTH SPEC", measuredWidth)
FirebaseCrashlytics.getInstance().setCustomKey("HEIGHT SPEC", measuredHeight)
FirebaseCrashlytics.getInstance().setCustomKey("VIEW measuredWidth", view.measuredWidth)
FirebaseCrashlytics.getInstance().setCustomKey("VIEW measuredHeight", view.measuredHeight)
FirebaseCrashlytics.getInstance().setCustomKey("WIDGET final width", widgetWidth)
FirebaseCrashlytics.getInstance().setCustomKey("WIDGET final height", widgetHeight)
}
return try { return try {
val btm = Bitmap.createBitmap( val btm = Bitmap.createBitmap(
widgetWidth, widgetWidth,
@ -53,6 +64,7 @@ object BitmapHelper {
} }
btm btm
} catch (ex: Exception) { } catch (ex: Exception) {
FirebaseCrashlytics.getInstance().recordException(ex)
Bitmap.createBitmap(5, 5, Bitmap.Config.ALPHA_8) Bitmap.createBitmap(5, 5, Bitmap.Config.ALPHA_8)
} }
} }

View File

@ -6,10 +6,17 @@ import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
import android.os.Looper
import android.util.Log
import androidx.core.provider.FontRequest import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat import androidx.core.provider.FontsContractCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tommasoberlose.anotherwidget.R import com.tommasoberlose.anotherwidget.R
import com.tommasoberlose.anotherwidget.db.EventRepository
import com.tommasoberlose.anotherwidget.global.Preferences import com.tommasoberlose.anotherwidget.global.Preferences
import com.tommasoberlose.anotherwidget.ui.widgets.MainWidget
import com.tommasoberlose.anotherwidget.utils.toPixel
import kotlin.math.min
object WidgetHelper { object WidgetHelper {
class WidgetSizeProvider( class WidgetSizeProvider(
@ -29,6 +36,8 @@ object WidgetHelper {
) )
val widthInPx = context.dip(width) val widthInPx = context.dip(width)
val heightInPx = context.dip(height) val heightInPx = context.dip(height)
FirebaseCrashlytics.getInstance().setCustomKey("widthInPx", widthInPx)
FirebaseCrashlytics.getInstance().setCustomKey("heightInPx", heightInPx)
return widthInPx to heightInPx return widthInPx to heightInPx
} }

View File

@ -0,0 +1,197 @@
package com.tommasoberlose.anotherwidget.receivers
import android.Manifest
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import com.chibatching.kotpref.Kotpref
import com.chibatching.kotpref.blockingBulk
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.fitness.Fitness
import com.google.android.gms.fitness.FitnessOptions
import com.google.android.gms.fitness.data.DataType
import com.google.android.gms.fitness.data.Field.FIELD_STEPS
import com.google.android.gms.fitness.request.DataReadRequest
import com.google.android.gms.location.*
import com.tommasoberlose.anotherwidget.global.Preferences
import com.tommasoberlose.anotherwidget.ui.widgets.MainWidget
import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission
import com.tommasoberlose.anotherwidget.utils.setExactIfCanSchedule
import java.util.*
import java.util.concurrent.TimeUnit
class ActivityDetectionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (ActivityTransitionResult.hasResult(intent)) {
val result = ActivityTransitionResult.extractResult(intent)!!
val lastEvent = result.transitionEvents.last()
if ((lastEvent.activityType == DetectedActivity.WALKING || lastEvent.activityType == DetectedActivity.RUNNING) && lastEvent.transitionType == ActivityTransition.ACTIVITY_TRANSITION_EXIT) {
requestDailySteps(context)
}
} else {
if ((intent.action == Intent.ACTION_BOOT_COMPLETED || intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) && Preferences.showDailySteps && (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || context.checkGrantedPermission(Manifest.permission.ACTIVITY_RECOGNITION))) {
resetDailySteps(context)
registerFence(context)
} else {
resetDailySteps(context)
}
}
}
companion object {
val FITNESS_OPTIONS: FitnessOptions = FitnessOptions.builder()
.addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
.addDataType(DataType.AGGREGATE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
.build()
fun registerFence(context: Context) {
Kotpref.init(context)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || context.checkGrantedPermission(
Manifest.permission.ACTIVITY_RECOGNITION)) {
val transitions = mutableListOf<ActivityTransition>()
transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()
transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.RUNNING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()
val request = ActivityTransitionRequest(transitions)
// myPendingIntent is the instance of PendingIntent where the app receives callbacks.
val task = ActivityRecognition.getClient(context)
.requestActivityTransitionUpdates(
request,
PendingIntent.getBroadcast(
context,
2,
Intent(context, ActivityDetectionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE
)
)
task.addOnFailureListener { e: Exception ->
e.printStackTrace()
Preferences.showDailySteps = false
}
}
}
fun unregisterFence(context: Context) {
val task = ActivityRecognition.getClient(context)
.removeActivityTransitionUpdates(
PendingIntent.getBroadcast(
context,
2,
Intent(context, ActivityDetectionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE
)
)
task.addOnCompleteListener {
if (it.isSuccessful) {
PendingIntent.getBroadcast(
context,
2,
Intent(context, ActivityDetectionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE
).cancel()
}
}
resetDailySteps(context)
clearTimeout(context)
}
fun requestDailySteps(context: Context) {
Kotpref.init(context)
val account: GoogleSignInAccount? = GoogleSignIn.getLastSignedInAccount(context)
if (account != null && GoogleSignIn.hasPermissions(account, FITNESS_OPTIONS)) {
val cal: Calendar = Calendar.getInstance()
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0)
val startTime: Long = cal.timeInMillis
cal.add(Calendar.DAY_OF_YEAR, 1)
val endTime: Long = cal.timeInMillis
val readRequest = DataReadRequest.Builder()
.aggregate(DataType.TYPE_STEP_COUNT_DELTA)
.aggregate(DataType.AGGREGATE_STEP_COUNT_DELTA)
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.bucketByTime(1, TimeUnit.DAYS)
.build()
Fitness.getHistoryClient(context, account)
.readData(readRequest)
.addOnSuccessListener { response ->
Preferences.googleFitSteps = response.buckets.sumBy {
try {
it.getDataSet(DataType.AGGREGATE_STEP_COUNT_DELTA)?.dataPoints?.get(
0
)?.getValue(FIELD_STEPS)?.asInt() ?: 0
} catch (ex: Exception) {
0
}
}.toLong()
MainWidget.updateWidget(context)
setTimeout(context)
}
}
}
private fun resetDailySteps(context: Context) {
Kotpref.init(context)
Preferences.blockingBulk {
remove(Preferences::googleFitSteps)
}
}
private fun setTimeout(context: Context) {
with(context.getSystemService(Context.ALARM_SERVICE) as AlarmManager) {
setExactIfCanSchedule(
AlarmManager.RTC,
Calendar.getInstance().timeInMillis + 5 * 60 * 1000,
PendingIntent.getBroadcast(
context,
5,
Intent(context, ActivityDetectionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE
)
)
}
}
private fun clearTimeout(context: Context) {
with(context.getSystemService(Context.ALARM_SERVICE) as AlarmManager) {
cancel(
PendingIntent.getBroadcast(
context,
5,
Intent(context, ActivityDetectionReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE
)
)
}
}
}
}

View File

@ -24,11 +24,7 @@ class WidgetClickListenerReceiver : BroadcastReceiver() {
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
val uri = Uri.parse("https://yandex.ru/pogoda") val uri = Uri.parse("https://www.google.com/search?q=weather")
.buildUpon()
.appendQueryParameter("lat", Preferences.customLocationLat)
.appendQueryParameter("lon", Preferences.customLocationLon)
.build()
val i = Intent(Intent.ACTION_VIEW, uri) val i = Intent(Intent.ACTION_VIEW, uri)
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
try { try {

View File

@ -66,11 +66,11 @@ class UpdateCalendarWorker(context: Context, params: WorkerParameters) : Worker(
limit.timeInMillis + limit.timeZone.getOffset(limit.timeInMillis).coerceAtLeast(0) limit.timeInMillis + limit.timeZone.getOffset(limit.timeInMillis).coerceAtLeast(0)
) )
if (data != null) { if (data != null) {
val filteredCalendarIdList = CalendarHelper.getFilteredCalendarIdList() val instances = data.list
for (instance in data.list) { for (instance in instances) {
try { try {
val e = provider.getEvent(instance.eventId) val e = provider.getEvent(instance.eventId)
if (e == null || e.deleted || filteredCalendarIdList.contains(e.calendarId)) if (e == null || e.deleted || CalendarHelper.getFilteredCalendarIdList().contains(e.calendarId))
continue continue
if (e.allDay) { if (e.allDay) {
val start = Calendar.getInstance() val start = Calendar.getInstance()
@ -131,12 +131,8 @@ class UpdateCalendarWorker(context: Context, params: WorkerParameters) : Worker(
eventRepository.resetNextEventData() eventRepository.resetNextEventData()
eventRepository.clearEvents() eventRepository.clearEvents()
} else { } else {
val first = filteredEventList.first() eventRepository.saveEvents(sortedEvents)
if (Preferences.nextEventId != first.id && ( eventRepository.saveNextEventData(filteredEventList.first())
//Preferences.showWeatherAsGlanceProvider || !Preferences.showNextEvent ||
eventRepository.getEventById(first.id)?.startDate != first.startDate))
eventRepository.saveNextEventData(first)
eventRepository.saveEvents(filteredEventList)
} }
} catch (ignored: java.lang.Exception) { } catch (ignored: java.lang.Exception) {
} }

View File

@ -10,11 +10,16 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.location.LocationServices
import com.tommasoberlose.anotherwidget.global.Preferences import com.tommasoberlose.anotherwidget.global.Preferences
import com.tommasoberlose.anotherwidget.network.WeatherNetworkApi import com.tommasoberlose.anotherwidget.network.WeatherNetworkApi
import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class WeatherWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { class WeatherWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
@ -24,6 +29,15 @@ class WeatherWorker(context: Context, params: WorkerParameters) : CoroutineWorke
if (Preferences.customLocationAdd == "" && if (Preferences.customLocationAdd == "" &&
context.checkGrantedPermission(Manifest.permission.ACCESS_COARSE_LOCATION) context.checkGrantedPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
) { ) {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
== ConnectionResult.SUCCESS
) {
suspendCancellableCoroutine { continuation ->
LocationServices.getFusedLocationProviderClient(context).lastLocation.addOnCompleteListener {
continuation.resume(if (it.isSuccessful) it.result else null)
}
}
} else {
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
var location: Location? = null var location: Location? = null
for (provider in lm.getProviders(true)) { for (provider in lm.getProviders(true)) {
@ -34,7 +48,8 @@ class WeatherWorker(context: Context, params: WorkerParameters) : CoroutineWorke
location = it location = it
} }
} }
location?.let { location -> location
}?.let { location ->
Preferences.customLocationLat = location.latitude.toString() Preferences.customLocationLat = location.latitude.toString()
Preferences.customLocationLon = location.longitude.toString() Preferences.customLocationLon = location.longitude.toString()
} }

View File

@ -0,0 +1,96 @@
package com.tommasoberlose.anotherwidget.ui.activities.settings
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient.BillingResponseCode.OK
import com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED
import com.tommasoberlose.anotherwidget.R
import com.tommasoberlose.anotherwidget.databinding.ActivitySupportDevBinding
import com.tommasoberlose.anotherwidget.ui.viewmodels.settings.SupportDevViewModel
import com.tommasoberlose.anotherwidget.utils.toast
import net.idik.lib.slimadapter.SlimAdapter
class SupportDevActivity : AppCompatActivity(), PurchasesUpdatedListener {
private lateinit var viewModel: SupportDevViewModel
private lateinit var adapter: SlimAdapter
private lateinit var binding: ActivitySupportDevBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(SupportDevViewModel::class.java)
viewModel.billingClient = BillingClient.newBuilder(this).enablePendingPurchases().setListener(this).build()
binding = ActivitySupportDevBinding.inflate(layoutInflater)
binding.listView.setHasFixedSize(true)
val mLayoutManager = LinearLayoutManager(this)
binding.listView.layoutManager = mLayoutManager
adapter = SlimAdapter.create()
adapter
.register<SkuDetails>(R.layout.inapp_product_layout) { item, injector ->
item.sku
injector
.with<TextView>(R.id.product_title) {
it.text = when (item.sku) {
"donation_coffee" -> getString(R.string.donation_coffee)
"donation_donuts" -> getString(R.string.donation_donuts)
"donation_breakfast" -> getString(R.string.donation_breakfast)
"donation_lunch" -> getString(R.string.donation_lunch)
else -> ""
}
}
.text(R.id.product_price, item.price)
.clicked(R.id.item) {
viewModel.purchase(this, item)
}
}
.attachTo(binding.listView)
viewModel.openConnection()
subscribeUi(viewModel)
binding.actionBack.setOnClickListener {
onBackPressed()
}
setContentView(binding.root)
}
private fun subscribeUi(viewModel: SupportDevViewModel) {
viewModel.products.observe(this, Observer {
if (it.isNotEmpty()) {
binding.loader.isVisible = false
}
adapter.updateData(it.sortedWith(compareBy(SkuDetails::getPriceAmountMicros)))
})
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
if (billingResult.responseCode == OK && purchases != null) {
for (purchase in purchases) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
viewModel.handlePurchase(purchase)
toast(getString(R.string.thanks))
}
}
} else if (billingResult.responseCode == USER_CANCELED) {
// DO nothing
} else {
toast(getString(R.string.error))
}
}
public override fun onDestroy() {
viewModel.closeConnection()
super.onDestroy()
}
}

View File

@ -8,10 +8,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.Navigation import androidx.navigation.Navigation
import androidx.transition.TransitionInflater
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import com.karumi.dexter.Dexter import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport import com.karumi.dexter.MultiplePermissionsReport
@ -29,10 +32,13 @@ import com.tommasoberlose.anotherwidget.helpers.MediaPlayerHelper
import com.tommasoberlose.anotherwidget.helpers.WeatherHelper import com.tommasoberlose.anotherwidget.helpers.WeatherHelper
import com.tommasoberlose.anotherwidget.ui.activities.settings.IntegrationsActivity import com.tommasoberlose.anotherwidget.ui.activities.settings.IntegrationsActivity
import com.tommasoberlose.anotherwidget.ui.activities.MainActivity import com.tommasoberlose.anotherwidget.ui.activities.MainActivity
import com.tommasoberlose.anotherwidget.ui.activities.settings.SupportDevActivity
import com.tommasoberlose.anotherwidget.ui.viewmodels.MainViewModel import com.tommasoberlose.anotherwidget.ui.viewmodels.MainViewModel
import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission import com.tommasoberlose.anotherwidget.utils.checkGrantedPermission
import com.tommasoberlose.anotherwidget.utils.ignoreExceptions
import com.tommasoberlose.anotherwidget.utils.openURI import com.tommasoberlose.anotherwidget.utils.openURI
import com.tommasoberlose.anotherwidget.utils.setOnSingleClickListener import com.tommasoberlose.anotherwidget.utils.setOnSingleClickListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -191,6 +197,10 @@ class SettingsFragment : Fragment() {
requireActivity().openURI("https://github.com/tommasoberlose/another-widget/blob/master/privacy-policy.md") requireActivity().openURI("https://github.com/tommasoberlose/another-widget/blob/master/privacy-policy.md")
} }
binding.actionHelpDev.setOnClickListener {
startActivity(Intent(requireContext(), SupportDevActivity::class.java))
}
binding.actionRefreshWidget.setOnClickListener { binding.actionRefreshWidget.setOnClickListener {
binding.actionRefreshIcon binding.actionRefreshIcon
.animate() .animate()

View File

@ -144,12 +144,6 @@ class GesturesFragment : Fragment() {
binding.showMultipleEventsToggle.setOnCheckedChangeListener { _, isChecked -> binding.showMultipleEventsToggle.setOnCheckedChangeListener { _, isChecked ->
Preferences.showNextEvent = isChecked Preferences.showNextEvent = isChecked
if (!isChecked) {
com.tommasoberlose.anotherwidget.db.EventRepository(requireContext()).run {
resetNextEventData()
close()
}
}
} }
binding.actionOpenEventDetails.setOnClickListener { binding.actionOpenEventDetails.setOnClickListener {

View File

@ -8,20 +8,26 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.Canvas import android.graphics.Canvas
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.view.animation.LayoutAnimationController
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.common.api.ApiException
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import com.tommasoberlose.anotherwidget.R import com.tommasoberlose.anotherwidget.R
@ -35,6 +41,8 @@ import com.tommasoberlose.anotherwidget.helpers.AlarmHelper
import com.tommasoberlose.anotherwidget.helpers.GlanceProviderHelper import com.tommasoberlose.anotherwidget.helpers.GlanceProviderHelper
import com.tommasoberlose.anotherwidget.helpers.MediaPlayerHelper import com.tommasoberlose.anotherwidget.helpers.MediaPlayerHelper
import com.tommasoberlose.anotherwidget.models.GlanceProvider import com.tommasoberlose.anotherwidget.models.GlanceProvider
import com.tommasoberlose.anotherwidget.receivers.ActivityDetectionReceiver
import com.tommasoberlose.anotherwidget.receivers.ActivityDetectionReceiver.Companion.FITNESS_OPTIONS
import com.tommasoberlose.anotherwidget.ui.activities.MainActivity import com.tommasoberlose.anotherwidget.ui.activities.MainActivity
import com.tommasoberlose.anotherwidget.ui.viewmodels.MainViewModel import com.tommasoberlose.anotherwidget.ui.viewmodels.MainViewModel
import com.tommasoberlose.anotherwidget.utils.* import com.tommasoberlose.anotherwidget.utils.*
@ -230,6 +238,40 @@ class GlanceTabFragment : Fragment() {
injector.visibility(R.id.info_icon, View.VISIBLE) injector.visibility(R.id.info_icon, View.VISIBLE)
isVisible = Preferences.customNotes != "" isVisible = Preferences.customNotes != ""
} }
Constants.GlanceProviderId.GOOGLE_FIT_STEPS -> {
val account: GoogleSignInAccount? = GoogleSignIn.getLastSignedInAccount(
context
)
if (GoogleSignIn.hasPermissions(
account,
FITNESS_OPTIONS
) && (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || requireActivity().checkGrantedPermission(
Manifest.permission.ACTIVITY_RECOGNITION
))
) {
injector.text(
R.id.label,
if (Preferences.showDailySteps) getString(R.string.settings_visible) else getString(
R.string.settings_not_visible
)
)
injector.visibility(R.id.error_icon, View.GONE)
injector.visibility(R.id.info_icon, View.VISIBLE)
isVisible = Preferences.showDailySteps
} else if (Preferences.showDailySteps) {
ActivityDetectionReceiver.unregisterFence(requireContext())
injector.visibility(R.id.error_icon, View.VISIBLE)
injector.visibility(R.id.info_icon, View.GONE)
injector.text(R.id.label, getString(R.string.settings_not_visible))
isVisible = false
} else {
ActivityDetectionReceiver.unregisterFence(requireContext())
injector.text(R.id.label, getString(R.string.settings_not_visible))
injector.visibility(R.id.error_icon, View.GONE)
injector.visibility(R.id.info_icon, View.VISIBLE)
isVisible = false
}
}
Constants.GlanceProviderId.EVENTS -> { Constants.GlanceProviderId.EVENTS -> {
isVisible = isVisible =
Preferences.showEventsAsGlanceProvider Preferences.showEventsAsGlanceProvider
@ -449,6 +491,30 @@ class GlanceTabFragment : Fragment() {
Preferences.showDailySteps = false Preferences.showDailySteps = false
} }
if (dialog != null) {
dialog?.show()
}
}
2 -> {
try {
val account: GoogleSignInAccount? = GoogleSignIn.getSignedInAccountFromIntent(
data
).getResult(ApiException::class.java)
if (!GoogleSignIn.hasPermissions(account, FITNESS_OPTIONS)) {
GoogleSignIn.requestPermissions(
requireActivity(),
1,
account,
FITNESS_OPTIONS
)
} else {
adapter.notifyItemRangeChanged(0, adapter.data.size)
}
} catch (e: ApiException) {
e.printStackTrace()
Preferences.showDailySteps = false
}
if (dialog != null) { if (dialog != null) {
dialog?.show() dialog?.show()
} }

View File

@ -0,0 +1,69 @@
package com.tommasoberlose.anotherwidget.ui.viewmodels.settings
import android.app.Activity
import android.content.Context
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.billingclient.api.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SupportDevViewModel : ViewModel() {
lateinit var billingClient: BillingClient
val products: MutableLiveData<List<SkuDetails>> = MutableLiveData(emptyList())
fun openConnection() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val params = SkuDetailsParams.newBuilder()
params.setSkusList(listOf("donation_coffee", "donation_donuts", "donation_breakfast", "donation_lunch", "donation_dinner")).setType(BillingClient.SkuType.INAPP)
viewModelScope.launch(Dispatchers.IO) {
val skuDetailsList = billingClient.querySkuDetails(params.build()).skuDetailsList
withContext(Dispatchers.Main) {
products.value = skuDetailsList
}
}
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
}
fun purchase(activity: Activity, product: SkuDetails) {
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(product)
.build()
billingClient.launchBillingFlow(activity, flowParams)
}
fun handlePurchase(purchase: Purchase) {
if (!purchase.isAcknowledged) {
viewModelScope.launch(Dispatchers.IO) {
val token = purchase.purchaseToken
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(token)
billingClient.acknowledgePurchase(acknowledgePurchaseParams.build())
val consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(token)
.build()
billingClient.consumePurchase(consumeParams)
}
}
}
fun closeConnection() {
billingClient.endConnection()
}
}

View File

@ -9,9 +9,10 @@ import androidx.lifecycle.viewModelScope
import com.koolio.library.DownloadableFontList import com.koolio.library.DownloadableFontList
import com.koolio.library.Font import com.koolio.library.Font
import com.koolio.library.FontList import com.koolio.library.FontList
//import com.tommasoberlose.anotherwidget.BuildConfig import com.tommasoberlose.anotherwidget.BuildConfig
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CustomFontViewModel(application: Application) : AndroidViewModel(application) { class CustomFontViewModel(application: Application) : AndroidViewModel(application) {
@ -33,7 +34,7 @@ class CustomFontViewModel(application: Application) : AndroidViewModel(applicati
} }
} }
// DownloadableFontList.requestDownloadableFontList(fontListCallback, BuildConfig.GOOGLE_API_KEY) DownloadableFontList.requestDownloadableFontList(fontListCallback, BuildConfig.GOOGLE_API_KEY)
} }
} }
} }

View File

@ -6,10 +6,12 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.text.format.DateUtils
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -494,7 +496,8 @@ class AlignedWidget(val context: Context, val rightAligned: Boolean = false) {
context, context,
now.timeInMillis, now.timeInMillis,
nextEvent.startDate nextEvent.startDate
).toLowerCase(Locale.getDefault()) )
.toLowerCase(Locale.getDefault())
} else { } else {
SettingsStringHelper.getAllDayEventDifferenceText( SettingsStringHelper.getAllDayEventDifferenceText(
context, context,
@ -507,18 +510,7 @@ class AlignedWidget(val context: Context, val rightAligned: Boolean = false) {
if (!Preferences.showNextEventOnMultipleLines) { if (!Preferences.showNextEventOnMultipleLines) {
bindingView.nextEventDifferenceTime.isVisible = true bindingView.nextEventDifferenceTime.isVisible = true
} else { } else {
val text = context.getString(R.string.events_glance_provider_format).format(nextEvent.title, diffTime) bindingView.nextEvent.text = context.getString(R.string.events_glance_provider_format).format(nextEvent.title, diffTime)
if (text.endsWith(diffTime)) {
bindingView.nextEvent.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
(v as TextView).layout?.run {
val diff = diffTime.trimStart();
val diffStart = text.length - diff.length
if (getLineStart(lineCount - 1) > diffStart)
v.text = (text.substring(0, diffStart).trimEnd() + '\n' + diff)
}
}
}
bindingView.nextEvent.text = text
bindingView.nextEventDifferenceTime.isVisible = false bindingView.nextEventDifferenceTime.isVisible = false
} }
} else { } else {
@ -585,7 +577,11 @@ class AlignedWidget(val context: Context, val rightAligned: Boolean = false) {
} else { } else {
val start = Calendar.getInstance().apply { timeInMillis = nextEvent.startDate } val start = Calendar.getInstance().apply { timeInMillis = nextEvent.startDate }
bindingView.subLineText.text = if (now.after(start)) { bindingView.subLineText.text = if (now.get(Calendar.DAY_OF_YEAR) == start.get(
Calendar.DAY_OF_YEAR)) {
DateHelper.getDateText(context, start)
} else if (now.get(Calendar.DAY_OF_YEAR) > start.get(Calendar.DAY_OF_YEAR) || now.get(
Calendar.YEAR) > start.get(Calendar.YEAR)) {
DateHelper.getDateText(context, now) DateHelper.getDateText(context, now)
} else { } else {
DateHelper.getDateText(context, start) DateHelper.getDateText(context, start)
@ -720,7 +716,8 @@ class AlignedWidget(val context: Context, val rightAligned: Boolean = false) {
context, context,
now.timeInMillis, now.timeInMillis,
nextEvent.startDate nextEvent.startDate
).toLowerCase(Locale.getDefault()) )
.toLowerCase(Locale.getDefault())
} else { } else {
SettingsStringHelper.getAllDayEventDifferenceText( SettingsStringHelper.getAllDayEventDifferenceText(
context, context,

View File

@ -11,6 +11,7 @@ import com.tommasoberlose.anotherwidget.global.Preferences
import com.tommasoberlose.anotherwidget.helpers.ColorHelper import com.tommasoberlose.anotherwidget.helpers.ColorHelper
import com.tommasoberlose.anotherwidget.helpers.IntentHelper import com.tommasoberlose.anotherwidget.helpers.IntentHelper
import com.tommasoberlose.anotherwidget.utils.isDarkTheme import com.tommasoberlose.anotherwidget.utils.isDarkTheme
import com.tommasoberlose.anotherwidget.utils.toPixel
class ClockWidget(val context: Context) { class ClockWidget(val context: Context) {
fun updateClockView(views: RemoteViews, widgetID: Int): RemoteViews { fun updateClockView(views: RemoteViews, widgetID: Int): RemoteViews {

View File

@ -6,10 +6,12 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.text.format.DateUtils
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.TextView import android.widget.TextView
@ -526,7 +528,8 @@ class StandardWidget(val context: Context) {
context, context,
now.timeInMillis, now.timeInMillis,
nextEvent.startDate nextEvent.startDate
).toLowerCase(Locale.getDefault()) )
.toLowerCase(Locale.getDefault())
} else { } else {
SettingsStringHelper.getAllDayEventDifferenceText( SettingsStringHelper.getAllDayEventDifferenceText(
context, context,
@ -539,18 +542,7 @@ class StandardWidget(val context: Context) {
if (!Preferences.showNextEventOnMultipleLines) { if (!Preferences.showNextEventOnMultipleLines) {
bindingView.nextEventDifferenceTime.isVisible = true bindingView.nextEventDifferenceTime.isVisible = true
} else { } else {
val text = context.getString(R.string.events_glance_provider_format).format(nextEvent.title, diffTime) bindingView.nextEvent.text = context.getString(R.string.events_glance_provider_format).format(nextEvent.title, diffTime)
if (text.endsWith(diffTime)) {
bindingView.nextEvent.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
(v as TextView).layout?.run {
val diff = diffTime.trimStart();
val diffStart = text.length - diff.length
if (getLineStart(lineCount - 1) > diffStart)
v.text = (text.substring(0, diffStart).trimEnd() + '\n' + diff)
}
}
}
bindingView.nextEvent.text = text
bindingView.nextEventDifferenceTime.isVisible = false bindingView.nextEventDifferenceTime.isVisible = false
} }
} else { } else {
@ -617,7 +609,11 @@ class StandardWidget(val context: Context) {
} else { } else {
val start = Calendar.getInstance().apply { timeInMillis = nextEvent.startDate } val start = Calendar.getInstance().apply { timeInMillis = nextEvent.startDate }
bindingView.subLineText.text = if (now.after(start)) { bindingView.subLineText.text = if (now.get(Calendar.DAY_OF_YEAR) == start.get(
Calendar.DAY_OF_YEAR)) {
DateHelper.getDateText(context, start)
} else if (now.get(Calendar.DAY_OF_YEAR) > start.get(Calendar.DAY_OF_YEAR) || now.get(
Calendar.YEAR) > start.get(Calendar.YEAR)) {
DateHelper.getDateText(context, now) DateHelper.getDateText(context, now)
} else { } else {
DateHelper.getDateText(context, start) DateHelper.getDateText(context, start)
@ -752,7 +748,8 @@ class StandardWidget(val context: Context) {
context, context,
now.timeInMillis, now.timeInMillis,
nextEvent.startDate nextEvent.startDate
).toLowerCase(Locale.getDefault()) )
.toLowerCase(Locale.getDefault())
} else { } else {
SettingsStringHelper.getAllDayEventDifferenceText( SettingsStringHelper.getAllDayEventDifferenceText(
context, context,

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

View File

@ -118,6 +118,14 @@
<string name="api_key_hint">Clave de API de OpenWeather</string> <string name="api_key_hint">Clave de API de OpenWeather</string>
<string name="default_weather_app">Clima de Google</string> <string name="default_weather_app">Clima de Google</string>
<string name="weather_warning">El clima de Google Awareness está obsoleto. ahora se requiere de una clave de API de OpenWeather para mostrar el clima en el widget.</string> <string name="weather_warning">El clima de Google Awareness está obsoleto. ahora se requiere de una clave de API de OpenWeather para mostrar el clima en el widget.</string>
<string name="api_key_title_1">Registrarse para obtener una cuenta de OpenWeather</string>
<string name="api_key_subtitle_1">Registra una cuenta gratis en OpenWeather. Solo tomará unos minutos.</string>
<string name="api_key_title_2">Copia tu clave de API</string>
<string name="api_key_subtitle_2">Encuentra el menu de claves API en los ajustes de tu cuenta y copia la clave predeterminada.</string>
<string name="api_key_title_3">Ingresa la clave a la app</string>
<string name="api_key_subtitle_3">Pega la clave en el campo de debajo y guárdalo. Una vez la clave sea activada el clima será visible.</string>
<string name="action_open_provider">Ir a OpenWeatherMap.com</string>
<string name="api_key_info_all_set">La activación de tu clave de API puede llegar a tomar unos diez minutos. El clima se actualizará tan pronto esté disponible.</string>
<string name="settings_weather_icon_pack_title">Paquete de iconos</string> <string name="settings_weather_icon_pack_title">Paquete de iconos</string>
<string name="settings_weather_icon_pack_default">Paquete de iconos %d</string> <string name="settings_weather_icon_pack_default">Paquete de iconos %d</string>
<string name="background_location_warning">Cuando el GPS está encendido, recolectamos los datos de la ubicación para actualizar la información del clima cuando la app está cerrada o no está en uso.\nNo usaremos esos datos en caso contrario.</string> <string name="background_location_warning">Cuando el GPS está encendido, recolectamos los datos de la ubicación para actualizar la información del clima cuando la app está cerrada o no está en uso.\nNo usaremos esos datos en caso contrario.</string>

View File

@ -97,6 +97,14 @@
<string name="api_key_hint">Kunci API OpenWeather</string> <string name="api_key_hint">Kunci API OpenWeather</string>
<string name="default_weather_app">Google Weather</string> <string name="default_weather_app">Google Weather</string>
<string name="weather_warning">Layanan cuaca Google Awareness telah ditutup. Sekarang membutuhkan Kunci API OpenWeather untuk menampilkan cuaca di widget.</string> <string name="weather_warning">Layanan cuaca Google Awareness telah ditutup. Sekarang membutuhkan Kunci API OpenWeather untuk menampilkan cuaca di widget.</string>
<string name="api_key_title_1">Daftar akun OpenWeather</string>
<string name="api_key_subtitle_1">Daftar akun gratis pada situs OpenWeather. Hanya akan membutuhkan waktu beberapa menit saja.</string>
<string name="api_key_title_2">Salin Kunci API anda</string>
<string name="api_key_subtitle_2">Temukan menu kunci API dari pengaturan akun anda dan salin kunci default.</string>
<string name="api_key_title_3">Masukkan kunci API ke aplikasi</string>
<string name="api_key_subtitle_3">Tempel kunci API ke dalam ruas di atas dan simpan. Setelah kunci diaktifkan, cuaca akan segera ditampilkan.</string>
<string name="action_open_provider">Kunjungi OpenWeatherMap.com</string>
<string name="api_key_info_all_set">Mungkin membutuhkan lebih kurang 10 menit sebelum kunci API anda diaktifkan. Cuaca akan diperbarui segera setelah data tersedia.</string>
<string name="settings_weather_icon_pack_title">Paket ikon</string> <string name="settings_weather_icon_pack_title">Paket ikon</string>
<string name="settings_weather_icon_pack_default">Paket ikon %d</string> <string name="settings_weather_icon_pack_default">Paket ikon %d</string>
@ -120,6 +128,7 @@
<!-- Glance --> <!-- Glance -->
<string name="settings_show_next_alarm_title">Waktu alarm berikutnya</string> <string name="settings_show_next_alarm_title">Waktu alarm berikutnya</string>
<string name="next_alarm_warning">Waktu alarm berikutnya sepertinya salah.\nTelah disetel %s.</string>
<string name="settings_at_a_glance_title">Sekilas</string> <string name="settings_at_a_glance_title">Sekilas</string>
<string name="settings_show_music_title">Lagu yang diputar</string> <string name="settings_show_music_title">Lagu yang diputar</string>
<string name="settings_request_notification_access">Membutuhkan izin akses notifikasi untuk menampilkan lagu yang saat ini diputar.</string> <string name="settings_request_notification_access">Membutuhkan izin akses notifikasi untuk menampilkan lagu yang saat ini diputar.</string>

View File

@ -117,6 +117,14 @@
<string name="api_key_hint">Chiave API OpenWeather</string> <string name="api_key_hint">Chiave API OpenWeather</string>
<string name="default_weather_app">Meteo Google</string> <string name="default_weather_app">Meteo Google</string>
<string name="weather_warning">Il meteo di Google Awareness è stato deprecato. È ora necessaria una chiave API OpenWeather per mostrare il meteo.</string> <string name="weather_warning">Il meteo di Google Awareness è stato deprecato. È ora necessaria una chiave API OpenWeather per mostrare il meteo.</string>
<string name="api_key_title_1">Registra un account su OpenWeather</string>
<string name="api_key_subtitle_1">Registra un account gratuito su OpenWeather, ci vorranno solo alcuni minuti.</string>
<string name="api_key_title_2">Copia la chiave API</string>
<string name="api_key_subtitle_2">Trova la chiave API nelle impostazioni del tuo nuovo account e copia la chiave di default.</string>
<string name="api_key_title_3">Inserisci la chiave nell\'app</string>
<string name="api_key_subtitle_3">Copia qui la chiave API e salva la nuova configurazione.</string>
<string name="action_open_provider">Apri OpenWeatherMap.com</string>
<string name="api_key_info_all_set">La chiave API potrebbe aver bisogno di più di 20 minuti per diventare attiva, aggiorneremo il meteo non appena sarà possibile.</string>
<string name="settings_weather_icon_pack_title">Icon pack</string> <string name="settings_weather_icon_pack_title">Icon pack</string>
<string name="settings_weather_icon_pack_default">Icon pack %d</string> <string name="settings_weather_icon_pack_default">Icon pack %d</string>
<string name="background_location_warning">Con la geolocalizzazione attiva raccoglieremo i dati sulla posizione per attivare l\'aggiornamento del meteo anche quando l\'app è chiusa o non in uso.\nNon utilizzeremo i tuoi dati in nessun altro modo.</string> <string name="background_location_warning">Con la geolocalizzazione attiva raccoglieremo i dati sulla posizione per attivare l\'aggiornamento del meteo anche quando l\'app è chiusa o non in uso.\nNon utilizzeremo i tuoi dati in nessun altro modo.</string>

View File

@ -94,6 +94,14 @@
<string name="api_key_hint">Ключ API OpenWeather</string> <string name="api_key_hint">Ключ API OpenWeather</string>
<string name="default_weather_app">Google Погода</string> <string name="default_weather_app">Google Погода</string>
<string name="weather_warning">Погода Google Awareness устарела. Для отображения погоды в виджете требуется API ключ OpenWeather.</string> <string name="weather_warning">Погода Google Awareness устарела. Для отображения погоды в виджете требуется API ключ OpenWeather.</string>
<string name="api_key_title_1">Создайте аккаунт OpenWeather</string>
<string name="api_key_subtitle_1">Создайте бесплатный аккаунт OpenWeather. Это займёт не более нескольких минут.</string>
<string name="api_key_title_2">Скопируйте ключ API</string>
<string name="api_key_subtitle_2">Найдие меню с ключами API в настройках аккаунта и скопируйте ключ.</string>
<string name="api_key_title_3">Введите ключ в приложении</string>
<string name="api_key_subtitle_3">Вставьте ключ в поле выше и сохраните. Как только ключ будет активирован, погода начнёт отображаться.</string>
<string name="action_open_provider">Перейти на OpenWeatherMap.com</string>
<string name="api_key_info_all_set">Активация ключа может занять до десяти минут. Погода обновится как только появится возможность.</string>
<string name="settings_weather_icon_pack_title">Набор иконок</string> <string name="settings_weather_icon_pack_title">Набор иконок</string>
<string name="settings_weather_icon_pack_default">Набор иконок %d</string> <string name="settings_weather_icon_pack_default">Набор иконок %d</string>
@ -117,6 +125,7 @@
<!-- Glance --> <!-- Glance -->
<string name="settings_show_next_alarm_title">Следующий будильник</string> <string name="settings_show_next_alarm_title">Следующий будильник</string>
<string name="next_alarm_warning">Проверьте следующий будильник.\nОн был установлен %s</string>
<string name="settings_at_a_glance_title">Glance</string> <string name="settings_at_a_glance_title">Glance</string>
<string name="settings_show_music_title">Текущая песня</string> <string name="settings_show_music_title">Текущая песня</string>
<string name="settings_request_notification_access">Нам необходим доступ к уведомлениям для отображения информации песни.</string> <string name="settings_request_notification_access">Нам необходим доступ к уведомлениям для отображения информации песни.</string>

View File

@ -107,6 +107,14 @@
<string name="api_key_hint">Kľúč k OpenWeather API</string> <string name="api_key_hint">Kľúč k OpenWeather API</string>
<string name="default_weather_app">Počasie Google</string> <string name="default_weather_app">Počasie Google</string>
<string name="weather_warning">Počasie Google Awareness je zastaralé. Pre zobrazovanie počasia vo widgete je vyžadovaný kľúč k OpenWeather API.</string> <string name="weather_warning">Počasie Google Awareness je zastaralé. Pre zobrazovanie počasia vo widgete je vyžadovaný kľúč k OpenWeather API.</string>
<string name="api_key_title_1">Vytvorte si OpenWeather účet</string>
<string name="api_key_subtitle_1">Zaregistrujte sa zdarma na OpenWeather. Bude to trvať len pár minút.</string>
<string name="api_key_title_2">Skopírujte kľúč k API</string>
<string name="api_key_subtitle_2">Nájdite menu s kľúčmi k API vo Vašom účte a skopírujte predvolený kľúč.</string>
<string name="api_key_title_3">Vložte kľúč do aplikácie</string>
<string name="api_key_subtitle_3">Vložte kľúč do poľa vyššie a uložte ho. Počasie sa zobrazí hneď ako bude kľúč aktivovaný.</string>
<string name="action_open_provider">Prejdite na OpenWeatherMap.com</string>
<string name="api_key_info_all_set">Aktivácia Vášho API kľúča môže trvať až 10 minút. Počasie sa aktualizuje hneď ako bude dostupné.</string>
<string name="settings_weather_icon_pack_title">Balíček ikon</string> <string name="settings_weather_icon_pack_title">Balíček ikon</string>
<string name="settings_weather_icon_pack_default">Balíček ikon %d</string> <string name="settings_weather_icon_pack_default">Balíček ikon %d</string>
<string name="background_location_warning">Zbierame údaje o polohe pre aktualizáciu počasia aj keď je aplikácia zatvorená alebo sa nepoužíva.\nInak tieto údaje nepoužijeme.</string> <string name="background_location_warning">Zbierame údaje o polohe pre aktualizáciu počasia aj keď je aplikácia zatvorená alebo sa nepoužíva.\nInak tieto údaje nepoužijeme.</string>
@ -131,6 +139,7 @@
<!-- Glance --> <!-- Glance -->
<string name="settings_show_next_alarm_title">Nasledujúci budík</string> <string name="settings_show_next_alarm_title">Nasledujúci budík</string>
<string name="next_alarm_warning">Nasledujúci budík vyzerá nesprávne.\nBol nastavený cez %s.</string>
<string name="settings_at_a_glance_title">Rýchle informácie</string> <string name="settings_at_a_glance_title">Rýchle informácie</string>
<string name="settings_show_music_title">Práve prehrávaná skladba</string> <string name="settings_show_music_title">Práve prehrávaná skladba</string>
<string name="settings_request_notification_access">Pre získanie informácií o práve prehrávanej skladbe potrebujeme povolenie prístupu k notifikáciám.</string> <string name="settings_request_notification_access">Pre získanie informácií o práve prehrávanej skladbe potrebujeme povolenie prístupu k notifikáciám.</string>

View File

@ -11,6 +11,10 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.2' classpath 'com.android.tools.build:gradle:7.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.8'
// Add the Crashlytics Gradle plugin.
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.6.1'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@ -8,6 +8,17 @@ If you choose to use my Service, then you agree to the collection and use of inf
The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Another Widget unless otherwise defined in this Privacy Policy. The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Another Widget unless otherwise defined in this Privacy Policy.
**Information Collection and Use**
For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information, including but not limited to Location data. The information that I request will be retained on your device and is not collected by me in any way.
The app does use third party services that may collect information used to identify you.
Link to privacy policy of third party service providers used by the app
* [Google Play Services](https://www.google.com/policies/privacy/)
* [Firebase Crashlytics](https://firebase.google.com/support/privacy/)
**Log Data** **Log Data**
I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.