Updated To M3

This commit is contained in:
edodson84 2022-07-03 08:34:40 -04:00
parent 56a8ac789b
commit d90bc385d9
806 changed files with 25211 additions and 26467 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

41
.gitignore vendored
View file

@ -1,15 +1,30 @@
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.gradle
/local.properties
.idea
.DS_Store
/build
/captures
/gradle
/gradlew
/gradlew.bat
/system_libs/*.jar
/keystore.properties
.idea/
# Keystore files
*.jks
/debug
/release
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof

View file

@ -1,40 +0,0 @@
android_app {
name: "DeskClock",
resource_dirs: ["res"],
sdk_version: "current",
overrides: ["AlarmClock"],
srcs: [
"src/**/*.java",
"gen/**/*.java",
],
product_specific: true,
static_libs: [
"androidx.annotation_annotation",
"androidx.collection_collection",
"androidx.arch.core_core-common",
"androidx.lifecycle_lifecycle-common",
"com.google.android.material_material",
"androidx.lifecycle_lifecycle-runtime",
"androidx.percentlayout_percentlayout",
"androidx.transition_transition",
"androidx.core_core",
"androidx.legacy_legacy-support-core-ui",
"androidx.media_media",
"androidx.legacy_legacy-support-v13",
"androidx.preference_preference",
"androidx.appcompat_appcompat",
"androidx.gridlayout_gridlayout",
"androidx.recyclerview_recyclerview",
],
required: [
"com.best.deskclock_whitelist"
],
}
prebuilt_etc {
name: "com.best.deskclock_whitelist",
product_specific: true,
sub_dir: "sysconfig",
src: "com.best.deskclock_whitelist.xml",
filename_from_src: true,
}

View file

@ -1,19 +0,0 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := DeskClockStudio
LOCAL_MODULE_CLASS := FAKE
LOCAL_MODULE_SUFFIX := -timestamp
gen_studio_tool_path := $(abspath $(LOCAL_PATH))/gen-studio.sh
system_libs_path := $(abspath $(LOCAL_PATH))/system_libs
system_libs_deps := $(call java-lib-deps, org.lineageos.platform.internal)
include $(BUILD_SYSTEM)/base_rules.mk
$(LOCAL_BUILT_MODULE): $(system_libs_deps)
$(hide) $(gen_studio_tool_path) "$(system_libs_path)" "$(system_libs_deps)"
$(hide) echo "Fake: $@"
$(hide) mkdir -p $(dir $@)
$(hide) touch $@

View file

@ -1,50 +0,0 @@
# Copyright (C) 2007 The Android Open Source Project
#
# 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.
#
# If you don't need to do a full clean build but would like to touch
# a file or delete some intermediate files, add a clean step to the end
# of the list. These steps will only be run once, if they haven't been
# run before.
#
# E.g.:
# $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
#
# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
# files that are missing or have been moved.
#
# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
# Use $(OUT_DIR) to refer to the "out" directory.
#
# If you need to re-do something that's already mentioned, just copy
# the command and add it to the bottom of the list. E.g., if a change
# that you made last week required touching a file and a change you
# made today requires touching the same file, just copy the old
# touch step and add it to the end of the list.
#
# ************************************************
# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
# ************************************************
# For example:
#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/AndroidTests_intermediates)
#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/core_intermediates)
#$(call add-clean-step, find $(OUT_DIR) -type f -name "IGTalkSession*" -print0 | xargs -0 rm -f)
#$(call add-clean-step, rm -rf $(PRODUCT_OUT)/data/*)
$(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/app/DeskClock)
# ************************************************
# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
# ************************************************

201
LICENSE
View file

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View file

190
NOTICE
View file

@ -1,190 +0,0 @@
Copyright (c) 2005-2008, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

3
OWNERS
View file

@ -1,3 +0,0 @@
# This project has no significant updates recently.
# Please update this list if you find better candidates.
rtenneti@google.com

View file

@ -1,10 +1,5 @@
# The best open source clock app out there. App includes alarm,clock, timer,stopwatch. Updated version of the aosp deskclock.
#Fdroid
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.best.deskclock/)
# Feature :
power off alarm (alarm will ring with phone off, snapdragon phones only, tested on all custom rom)
@ -17,4 +12,6 @@ swipe to delete alarm
Light and Dark mode
Updated to M3 styling

67
app/build.gradle Normal file
View file

@ -0,0 +1,67 @@
plugins {
id 'com.android.application'
}
android {
signingConfigs {
Retail {
storeFile file('C:\\Users\\Fissy\\Desktop\\apktool\\tools\\keystore\\Retail.jks')
storePassword 'Qpalzm1784!'
keyAlias 'Retail'
keyPassword 'Qpalzm1784!'
}
}
compileSdk 32
defaultConfig {
vectorDrawables.useSupportLibrary = true
applicationId "com.best.deskclock"
minSdk 23
targetSdk 25
versionCode 1
versionName '1'
signingConfig signingConfigs.Retail
}
buildTypes {
Retail {
debuggable true
signingConfig signingConfigs.Retail
minifyEnabled true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildToolsVersion '32.1.0-rc1'
}
dependencies {
implementation 'androidx.annotation:annotation:1.4.0'
implementation "androidx.collection:collection:1.2.0"
implementation "androidx.arch.core:core-common:2.1.0"
implementation 'androidx.lifecycle:lifecycle-common:2.5.0'
implementation "androidx.lifecycle:lifecycle-runtime:2.4.1"
implementation "androidx.transition:transition:1.4.1"
implementation "androidx.core:core:1.8.0"
implementation "androidx.legacy:legacy-support-core-ui:1.0.0"
implementation "androidx.media:media:1.6.0"
implementation "androidx.legacy:legacy-support-v13:1.0.0"
implementation "androidx.preference:preference:1.2.0"
implementation "androidx.appcompat:appcompat:1.4.2"
implementation "androidx.gridlayout:gridlayout:1.0.0"
implementation "androidx.percentlayout:percentlayout:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation 'com.google.android.material:material:1.7.0-alpha02'
implementation 'androidx.compose.material3:material3:1.0.0-alpha13'
implementation 'androidx.compose.material3:material3-window-size-class:1.0.0-alpha13'
implementation 'androidx.appcompat:appcompat:1.0.0'
}
/*gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
}*/

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -15,13 +15,9 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.best.deskclock">
<original-package android:name="com.best.alarmclock" />
<original-package android:name="com.best.deskclock" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
@ -29,8 +25,10 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="org.codeaurora.permission.POWER_OFF_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission
android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS"
tools:ignore="ProtectedPermissions" />
<application
android:name=".DeskClockApplication"
@ -39,11 +37,12 @@
android:backupAgent="DeskClockBackupAgent"
android:fullBackupContent="@xml/backup_scheme"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher_alarmclock"
android:icon="@mipmap/launcher_clock"
android:label="@string/app_label"
android:requiredForAllUsers="true"
android:supportsRtl="true"
android:theme="@style/Theme.DeskClock">
android:theme="@style/Theme.DeskClock"
tools:targetApi="o">
<!-- ============================================================== -->
<!-- Main app components. -->
@ -51,10 +50,9 @@
<activity
android:name=".DeskClock"
android:label="@string/app_label"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan"
android:exported="true">
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -66,8 +64,9 @@
<activity
android:name=".ringtone.RingtonePickerActivity"
android:excludeFromRecents="true"
android:parentActivityName=".DeskClock"
android:taskAffinity=""
android:theme="@style/Theme.DeskClock.RingtonePicker" />
android:theme="@style/Theme.DeskClock.Actionbar" />
<activity
android:name=".worldclock.CitySelectionActivity"
@ -75,7 +74,7 @@
android:label="@string/cities_activity_title"
android:parentActivityName=".DeskClock"
android:taskAffinity=""
android:theme="@style/Theme.DeskClock.CitySelection" />
android:theme="@style/Theme.DeskClock.Actionbar" />
<activity
android:name=".settings.SettingsActivity"
@ -83,7 +82,7 @@
android:label="@string/settings"
android:parentActivityName=".DeskClock"
android:taskAffinity=""
android:theme="@style/Theme.DeskClock.Settings" />
android:theme="@style/Theme.DeskClock.Actionbar" />
<activity
android:name=".HandleShortcuts"
@ -99,6 +98,7 @@
<activity
android:name="com.best.deskclock.HandleApiCalls"
android:excludeFromRecents="true"
android:exported="true"
android:permission="com.android.alarm.permission.SET_ALARM"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
@ -122,7 +122,7 @@
<activity-alias
android:name="HandleSetAlarm"
android:exported="true"
android:targetActivity="com.best.deskclock.HandleApiCalls"/>
android:targetActivity="com.best.deskclock.HandleApiCalls" />
<!-- ============================================================== -->
<!-- Alarm components. -->
@ -137,12 +137,14 @@
android:showWhenLocked="true"
android:taskAffinity=""
android:theme="@style/Theme.DeskClock.Wallpaper"
android:windowSoftInputMode="stateAlwaysHidden" />
android:windowSoftInputMode="stateAlwaysHidden"
tools:ignore="NonResizeableActivity"
tools:targetApi="o_mr1" />
<activity
android:name=".AlarmSelectionActivity"
android:label="@string/dismiss_alarm"
android:theme="@android:style/Theme.Holo.Light.Dialog.NoActionBar" />
android:theme="@android:style/Theme.Material.Dialog.NoActionBar" />
<provider
android:name=".provider.ClockProvider"
@ -185,7 +187,8 @@
android:launchMode="singleInstance"
android:resizeableActivity="false"
android:showOnLockScreen="true"
android:taskAffinity="" />
android:taskAffinity=""
tools:ignore="NonResizeableActivity" />
<!-- Legacy broadcast receiver that honors old scheduled timers across app upgrade. -->
<receiver
@ -219,20 +222,22 @@
android:name=".ScreensaverActivity"
android:excludeFromRecents="true"
android:resizeableActivity="false"
android:taskAffinity="" />
android:taskAffinity=""
tools:ignore="NonResizeableActivity" />
<activity
android:name=".settings.ScreensaverSettingsActivity"
android:excludeFromRecents="true"
android:label="@string/screensaver_settings"
android:parentActivityName=".DeskClock"
android:taskAffinity=""
android:theme="@style/Theme.DeskClock.Settings" />
android:theme="@style/Theme.DeskClock.Actionbar" />
<service
android:name=".Screensaver"
android:exported="false"
android:label="@string/app_label"
android:permission="android.permission.BIND_DREAM_SERVICE"
android:exported="false">
android:permission="android.permission.BIND_DREAM_SERVICE">
<intent-filter>
<action android:name="android.service.dreams.DreamService" />
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
@ -250,8 +255,8 @@
<receiver
android:name="com.best.alarmclock.AnalogAppWidgetProvider"
android:label="@string/analog_gadget"
android:exported="false">
android:exported="false"
android:label="@string/analog_gadget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@ -266,8 +271,8 @@
<receiver
android:name="com.best.alarmclock.DigitalAppWidgetProvider"
android:label="@string/digital_gadget"
android:exported="false">
android:exported="false"
android:label="@string/digital_gadget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />

View file

@ -16,6 +16,10 @@
package com.best.alarmclock;
import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
import static java.util.Calendar.DAY_OF_WEEK;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
@ -38,10 +42,6 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
import static java.util.Calendar.DAY_OF_WEEK;
/**
* This factory produces entries in the world cities list view displayed at the bottom of the
* digital widget. Each row is comprised of two world cities located side-by-side.
@ -84,7 +84,7 @@ public class DigitalAppWidgetCityViewsFactory implements RemoteViewsFactory {
/**
* <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
* mShowHomeClock.</p>
*
* <p>
* {@inheritDoc}
*/
@Override
@ -100,7 +100,7 @@ public class DigitalAppWidgetCityViewsFactory implements RemoteViewsFactory {
/**
* <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
* mShowHomeClock.</p>
*
* <p>
* {@inheritDoc}
*/
@Override
@ -161,7 +161,7 @@ public class DigitalAppWidgetCityViewsFactory implements RemoteViewsFactory {
/**
* <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
* mShowHomeClock.</p>
*
* <p>
* {@inheritDoc}
*/
@Override

View file

@ -16,44 +16,6 @@
package com.best.alarmclock;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.ArraySet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.TextClock;
import android.widget.TextView;
import com.best.deskclock.DeskClock;
import com.best.deskclock.LogUtils;
import com.best.deskclock.R;
import com.best.deskclock.Utils;
import com.best.deskclock.data.City;
import com.best.deskclock.data.DataModel;
import com.best.deskclock.uidata.UiDataModel;
import com.best.deskclock.worldclock.CitySelectionActivity;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
import static android.app.PendingIntent.FLAG_NO_CREATE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
@ -75,21 +37,61 @@ import static com.best.deskclock.data.DataModel.ACTION_WORLD_CITIES_CHANGED;
import static java.lang.Math.max;
import static java.lang.Math.round;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.ArraySet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.TextClock;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.best.deskclock.DeskClock;
import com.best.deskclock.LogUtils;
import com.best.deskclock.R;
import com.best.deskclock.Utils;
import com.best.deskclock.data.City;
import com.best.deskclock.data.DataModel;
import com.best.deskclock.uidata.UiDataModel;
import com.best.deskclock.worldclock.CitySelectionActivity;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
/**
* <p>This provider produces a widget resembling one of the formats below.</p>
*
* <p>
* If an alarm is scheduled to ring in the future:
* <pre>
* 12:59 AM
* WED, FEB 3 THU 9:30 AM
* </pre>
*
* <p>
* If no alarm is scheduled to ring in the future:
* <pre>
* 12:59 AM
* WED, FEB 3
* </pre>
*
* <p>
* This widget is scaling the font sizes to fit within the widget bounds chosen by the user without
* any clipping. To do so it measures layouts offscreen using a range of font sizes in order to
* choose optimal values.
@ -105,9 +107,239 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
*/
private static final String ACTION_ON_DAY_CHANGE = "com.best.deskclock.ON_DAY_CHANGE";
/** Intent used to deliver the {@link #ACTION_ON_DAY_CHANGE} callback. */
/**
* Intent used to deliver the {@link #ACTION_ON_DAY_CHANGE} callback.
*/
private static final Intent DAY_CHANGE_INTENT = new Intent(ACTION_ON_DAY_CHANGE);
/**
* Compute optimal font and icon sizes offscreen for both portrait and landscape orientations
* using the last known widget size and apply them to the widget.
*/
private static void relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
Bundle options) {
final RemoteViews portrait = relayoutWidget(context, wm, widgetId, options, true);
final RemoteViews landscape = relayoutWidget(context, wm, widgetId, options, false);
final RemoteViews widget = new RemoteViews(landscape, portrait);
wm.updateAppWidget(widgetId, widget);
wm.notifyAppWidgetViewDataChanged(widgetId, R.id.world_city_list);
}
/**
* Compute optimal font and icon sizes offscreen for the given orientation.
*/
private static RemoteViews relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
Bundle options, boolean portrait) {
// Create a remote view for the digital clock.
final String packageName = context.getPackageName();
final RemoteViews rv = new RemoteViews(packageName, R.layout.digital_widget);
// Tapping on the widget opens the app (if not on the lock screen).
if (Utils.isWidgetClickable(wm, widgetId)) {
final Intent openApp = new Intent(context, DeskClock.class);
final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
rv.setOnClickPendingIntent(R.id.digital_widget, pi);
}
// Configure child views of the remote view.
final CharSequence dateFormat = getDateFormat(context);
rv.setCharSequence(R.id.date, "setFormat12Hour", dateFormat);
rv.setCharSequence(R.id.date, "setFormat24Hour", dateFormat);
final String nextAlarmTime = Utils.getNextAlarm(context);
if (TextUtils.isEmpty(nextAlarmTime)) {
rv.setViewVisibility(R.id.nextAlarm, GONE);
rv.setViewVisibility(R.id.nextAlarmIcon, GONE);
} else {
rv.setTextViewText(R.id.nextAlarm, nextAlarmTime);
rv.setViewVisibility(R.id.nextAlarm, VISIBLE);
rv.setViewVisibility(R.id.nextAlarmIcon, VISIBLE);
}
if (options == null) {
options = wm.getAppWidgetOptions(widgetId);
}
// Fetch the widget size selected by the user.
final Resources resources = context.getResources();
final float density = resources.getDisplayMetrics().density;
final int minWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
final int minHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
final int maxWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
final int maxHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
final int targetWidthPx = portrait ? minWidthPx : maxWidthPx;
final int targetHeightPx = portrait ? maxHeightPx : minHeightPx;
final int largestClockFontSizePx =
resources.getDimensionPixelSize(R.dimen.widget_max_clock_font_size);
// Create a size template that describes the widget bounds.
final Sizes template = new Sizes(targetWidthPx, targetHeightPx, largestClockFontSizePx);
// Compute optimal font sizes and icon sizes to fit within the widget bounds.
final Sizes sizes = optimizeSizes(context, template, nextAlarmTime);
if (LOGGER.isVerboseLoggable()) {
LOGGER.v(sizes.toString());
}
// Apply the computed sizes to the remote views.
rv.setImageViewBitmap(R.id.nextAlarmIcon, sizes.mIconBitmap);
rv.setTextViewTextSize(R.id.date, COMPLEX_UNIT_PX, sizes.mFontSizePx);
rv.setTextViewTextSize(R.id.nextAlarm, COMPLEX_UNIT_PX, sizes.mFontSizePx);
rv.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx);
final int smallestWorldCityListSizePx =
resources.getDimensionPixelSize(R.dimen.widget_min_world_city_list_size);
if (sizes.getListHeight() <= smallestWorldCityListSizePx) {
// Insufficient space; hide the world city list.
rv.setViewVisibility(R.id.world_city_list, GONE);
} else {
// Set an adapter on the world city list. That adapter connects to a Service via intent.
final Intent intent = new Intent(context, DigitalAppWidgetCityService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
rv.setRemoteAdapter(R.id.world_city_list, intent);
rv.setViewVisibility(R.id.world_city_list, VISIBLE);
// Tapping on the widget opens the city selection activity (if not on the lock screen).
if (Utils.isWidgetClickable(wm, widgetId)) {
final Intent selectCity = new Intent(context, CitySelectionActivity.class);
final PendingIntent pi = PendingIntent.getActivity(context, 0, selectCity, 0);
rv.setPendingIntentTemplate(R.id.world_city_list, pi);
}
}
return rv;
}
/**
* Inflate an offscreen copy of the widget views. Binary search through the range of sizes until
* the optimal sizes that fit within the widget bounds are located.
*/
private static Sizes optimizeSizes(Context context, Sizes template, String nextAlarmTime) {
// Inflate a test layout to compute sizes at different font sizes.
final LayoutInflater inflater = LayoutInflater.from(context);
@SuppressLint("InflateParams") final View sizer = inflater.inflate(R.layout.digital_widget_sizer, null /* root */);
// Configure the date to display the current date string.
final CharSequence dateFormat = getDateFormat(context);
final TextClock date = sizer.findViewById(R.id.date);
date.setFormat12Hour(dateFormat);
date.setFormat24Hour(dateFormat);
// Configure the next alarm views to display the next alarm time or be gone.
final TextView nextAlarmIcon = sizer.findViewById(R.id.nextAlarmIcon);
final TextView nextAlarm = sizer.findViewById(R.id.nextAlarm);
if (TextUtils.isEmpty(nextAlarmTime)) {
nextAlarm.setVisibility(GONE);
nextAlarmIcon.setVisibility(GONE);
} else {
nextAlarm.setText(nextAlarmTime);
nextAlarm.setVisibility(VISIBLE);
nextAlarmIcon.setVisibility(VISIBLE);
nextAlarmIcon.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
}
// Measure the widget at the largest possible size.
Sizes high = measure(template, template.getLargestClockFontSizePx(), sizer);
if (!high.hasViolations()) {
return high;
}
// Measure the widget at the smallest possible size.
Sizes low = measure(template, template.getSmallestClockFontSizePx(), sizer);
if (low.hasViolations()) {
return low;
}
// Binary search between the smallest and largest sizes until an optimum size is found.
while (low.getClockFontSizePx() != high.getClockFontSizePx()) {
final int midFontSize = (low.getClockFontSizePx() + high.getClockFontSizePx()) / 2;
if (midFontSize == low.getClockFontSizePx()) {
return low;
}
final Sizes midSize = measure(template, midFontSize, sizer);
if (midSize.hasViolations()) {
high = midSize;
} else {
low = midSize;
}
}
return low;
}
private static AlarmManager getAlarmManager(Context context) {
return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
}
/**
* Compute all font and icon sizes based on the given {@code clockFontSize} and apply them to
* the offscreen {@code sizer} view. Measure the {@code sizer} view and return the resulting
* size measurements.
*/
private static Sizes measure(Sizes template, int clockFontSize, View sizer) {
// Create a copy of the given template sizes.
final Sizes measuredSizes = template.newSize();
// Configure the clock to display the widest time string.
final TextClock date = sizer.findViewById(R.id.date);
final TextClock clock = sizer.findViewById(R.id.clock);
final TextView nextAlarm = sizer.findViewById(R.id.nextAlarm);
final TextView nextAlarmIcon = sizer.findViewById(R.id.nextAlarmIcon);
// Adjust the font sizes.
measuredSizes.setClockFontSizePx(clockFontSize);
clock.setText(getLongestTimeString(clock));
clock.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mClockFontSizePx);
date.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
nextAlarm.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
nextAlarmIcon.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mIconFontSizePx);
nextAlarmIcon.setPadding(measuredSizes.mIconPaddingPx, 0, measuredSizes.mIconPaddingPx, 0);
// Measure and layout the sizer.
final int widthSize = View.MeasureSpec.getSize(measuredSizes.mTargetWidthPx);
final int heightSize = View.MeasureSpec.getSize(measuredSizes.mTargetHeightPx);
final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(widthSize, UNSPECIFIED);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(heightSize, UNSPECIFIED);
sizer.measure(widthMeasureSpec, heightMeasureSpec);
sizer.layout(0, 0, sizer.getMeasuredWidth(), sizer.getMeasuredHeight());
// Copy the measurements into the result object.
measuredSizes.mMeasuredWidthPx = sizer.getMeasuredWidth();
measuredSizes.mMeasuredHeightPx = sizer.getMeasuredHeight();
measuredSizes.mMeasuredTextClockWidthPx = clock.getMeasuredWidth();
measuredSizes.mMeasuredTextClockHeightPx = clock.getMeasuredHeight();
// If an alarm icon is required, generate one from the TextView with the special font.
if (nextAlarmIcon.getVisibility() == VISIBLE) {
measuredSizes.mIconBitmap = Utils.createBitmap(nextAlarmIcon);
}
return measuredSizes;
}
/**
* @return "11:59" or "23:59" in the current locale
*/
private static CharSequence getLongestTimeString(TextClock clock) {
final CharSequence format = clock.is24HourModeEnabled()
? clock.getFormat24Hour()
: clock.getFormat12Hour();
final Calendar longestPMTime = Calendar.getInstance();
longestPMTime.set(0, 0, 0, 23, 59);
return DateFormat.format(format, longestPMTime);
}
/**
* @return the locale-specific date pattern
*/
private static String getDateFormat(Context context) {
final Locale locale = Locale.getDefault();
final String skeleton = context.getString(R.string.abbrev_wday_month_day_no_year);
return DateFormat.getBestDateTimePattern(locale, skeleton);
}
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
@ -178,171 +410,13 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
*/
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager wm, int widgetId,
Bundle options) {
Bundle options) {
super.onAppWidgetOptionsChanged(context, wm, widgetId, options);
// scale the fonts of the clock to fit inside the new size
relayoutWidget(context, AppWidgetManager.getInstance(context), widgetId, options);
}
/**
* Compute optimal font and icon sizes offscreen for both portrait and landscape orientations
* using the last known widget size and apply them to the widget.
*/
private static void relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
Bundle options) {
final RemoteViews portrait = relayoutWidget(context, wm, widgetId, options, true);
final RemoteViews landscape = relayoutWidget(context, wm, widgetId, options, false);
final RemoteViews widget = new RemoteViews(landscape, portrait);
wm.updateAppWidget(widgetId, widget);
wm.notifyAppWidgetViewDataChanged(widgetId, R.id.world_city_list);
}
/**
* Compute optimal font and icon sizes offscreen for the given orientation.
*/
private static RemoteViews relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
Bundle options, boolean portrait) {
// Create a remote view for the digital clock.
final String packageName = context.getPackageName();
final RemoteViews rv = new RemoteViews(packageName, R.layout.digital_widget);
// Tapping on the widget opens the app (if not on the lock screen).
if (Utils.isWidgetClickable(wm, widgetId)) {
final Intent openApp = new Intent(context, DeskClock.class);
final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
rv.setOnClickPendingIntent(R.id.digital_widget, pi);
}
// Configure child views of the remote view.
final CharSequence dateFormat = getDateFormat(context);
rv.setCharSequence(R.id.date, "setFormat12Hour", dateFormat);
rv.setCharSequence(R.id.date, "setFormat24Hour", dateFormat);
final String nextAlarmTime = Utils.getNextAlarm(context);
if (TextUtils.isEmpty(nextAlarmTime)) {
rv.setViewVisibility(R.id.nextAlarm, GONE);
rv.setViewVisibility(R.id.nextAlarmIcon, GONE);
} else {
rv.setTextViewText(R.id.nextAlarm, nextAlarmTime);
rv.setViewVisibility(R.id.nextAlarm, VISIBLE);
rv.setViewVisibility(R.id.nextAlarmIcon, VISIBLE);
}
if (options == null) {
options = wm.getAppWidgetOptions(widgetId);
}
// Fetch the widget size selected by the user.
final Resources resources = context.getResources();
final float density = resources.getDisplayMetrics().density;
final int minWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
final int minHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
final int maxWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
final int maxHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
final int targetWidthPx = portrait ? minWidthPx : maxWidthPx;
final int targetHeightPx = portrait ? maxHeightPx : minHeightPx;
final int largestClockFontSizePx =
resources.getDimensionPixelSize(R.dimen.widget_max_clock_font_size);
// Create a size template that describes the widget bounds.
final Sizes template = new Sizes(targetWidthPx, targetHeightPx, largestClockFontSizePx);
// Compute optimal font sizes and icon sizes to fit within the widget bounds.
final Sizes sizes = optimizeSizes(context, template, nextAlarmTime);
if (LOGGER.isVerboseLoggable()) {
LOGGER.v(sizes.toString());
}
// Apply the computed sizes to the remote views.
rv.setImageViewBitmap(R.id.nextAlarmIcon, sizes.mIconBitmap);
rv.setTextViewTextSize(R.id.date, COMPLEX_UNIT_PX, sizes.mFontSizePx);
rv.setTextViewTextSize(R.id.nextAlarm, COMPLEX_UNIT_PX, sizes.mFontSizePx);
rv.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx);
final int smallestWorldCityListSizePx =
resources.getDimensionPixelSize(R.dimen.widget_min_world_city_list_size);
if (sizes.getListHeight() <= smallestWorldCityListSizePx) {
// Insufficient space; hide the world city list.
rv.setViewVisibility(R.id.world_city_list, GONE);
} else {
// Set an adapter on the world city list. That adapter connects to a Service via intent.
final Intent intent = new Intent(context, DigitalAppWidgetCityService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
rv.setRemoteAdapter(R.id.world_city_list, intent);
rv.setViewVisibility(R.id.world_city_list, VISIBLE);
// Tapping on the widget opens the city selection activity (if not on the lock screen).
if (Utils.isWidgetClickable(wm, widgetId)) {
final Intent selectCity = new Intent(context, CitySelectionActivity.class);
final PendingIntent pi = PendingIntent.getActivity(context, 0, selectCity, 0);
rv.setPendingIntentTemplate(R.id.world_city_list, pi);
}
}
return rv;
}
/**
* Inflate an offscreen copy of the widget views. Binary search through the range of sizes until
* the optimal sizes that fit within the widget bounds are located.
*/
private static Sizes optimizeSizes(Context context, Sizes template, String nextAlarmTime) {
// Inflate a test layout to compute sizes at different font sizes.
final LayoutInflater inflater = LayoutInflater.from(context);
@SuppressLint("InflateParams")
final View sizer = inflater.inflate(R.layout.digital_widget_sizer, null /* root */);
// Configure the date to display the current date string.
final CharSequence dateFormat = getDateFormat(context);
final TextClock date = (TextClock) sizer.findViewById(R.id.date);
date.setFormat12Hour(dateFormat);
date.setFormat24Hour(dateFormat);
// Configure the next alarm views to display the next alarm time or be gone.
final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon);
final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm);
if (TextUtils.isEmpty(nextAlarmTime)) {
nextAlarm.setVisibility(GONE);
nextAlarmIcon.setVisibility(GONE);
} else {
nextAlarm.setText(nextAlarmTime);
nextAlarm.setVisibility(VISIBLE);
nextAlarmIcon.setVisibility(VISIBLE);
nextAlarmIcon.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
}
// Measure the widget at the largest possible size.
Sizes high = measure(template, template.getLargestClockFontSizePx(), sizer);
if (!high.hasViolations()) {
return high;
}
// Measure the widget at the smallest possible size.
Sizes low = measure(template, template.getSmallestClockFontSizePx(), sizer);
if (low.hasViolations()) {
return low;
}
// Binary search between the smallest and largest sizes until an optimum size is found.
while (low.getClockFontSizePx() != high.getClockFontSizePx()) {
final int midFontSize = (low.getClockFontSizePx() + high.getClockFontSizePx()) / 2;
if (midFontSize == low.getClockFontSizePx()) {
return low;
}
final Sizes midSize = measure(template, midFontSize, sizer);
if (midSize.hasViolations()) {
high = midSize;
} else {
low = midSize;
}
}
return low;
}
/**
* Remove the existing day-change callback if it is not needed (no selected cities exist).
* Add the day-change callback if it is needed (selected cities exist).
@ -371,7 +445,7 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
// Schedule the next day-change callback; at least one city is displayed.
final PendingIntent pi =
PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_UPDATE_CURRENT);
getAlarmManager(context).setExact(AlarmManager.RTC, nextDay.getTime(), pi);
getAlarmManager(context).setExact(AlarmManager.RTC, Objects.requireNonNull(nextDay).getTime(), pi);
}
/**
@ -386,77 +460,6 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
}
}
private static AlarmManager getAlarmManager(Context context) {
return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
}
/**
* Compute all font and icon sizes based on the given {@code clockFontSize} and apply them to
* the offscreen {@code sizer} view. Measure the {@code sizer} view and return the resulting
* size measurements.
*/
private static Sizes measure(Sizes template, int clockFontSize, View sizer) {
// Create a copy of the given template sizes.
final Sizes measuredSizes = template.newSize();
// Configure the clock to display the widest time string.
final TextClock date = (TextClock) sizer.findViewById(R.id.date);
final TextClock clock = (TextClock) sizer.findViewById(R.id.clock);
final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm);
final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon);
// Adjust the font sizes.
measuredSizes.setClockFontSizePx(clockFontSize);
clock.setText(getLongestTimeString(clock));
clock.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mClockFontSizePx);
date.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
nextAlarm.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
nextAlarmIcon.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mIconFontSizePx);
nextAlarmIcon.setPadding(measuredSizes.mIconPaddingPx, 0, measuredSizes.mIconPaddingPx, 0);
// Measure and layout the sizer.
final int widthSize = View.MeasureSpec.getSize(measuredSizes.mTargetWidthPx);
final int heightSize = View.MeasureSpec.getSize(measuredSizes.mTargetHeightPx);
final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(widthSize, UNSPECIFIED);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(heightSize, UNSPECIFIED);
sizer.measure(widthMeasureSpec, heightMeasureSpec);
sizer.layout(0, 0, sizer.getMeasuredWidth(), sizer.getMeasuredHeight());
// Copy the measurements into the result object.
measuredSizes.mMeasuredWidthPx = sizer.getMeasuredWidth();
measuredSizes.mMeasuredHeightPx = sizer.getMeasuredHeight();
measuredSizes.mMeasuredTextClockWidthPx = clock.getMeasuredWidth();
measuredSizes.mMeasuredTextClockHeightPx = clock.getMeasuredHeight();
// If an alarm icon is required, generate one from the TextView with the special font.
if (nextAlarmIcon.getVisibility() == VISIBLE) {
measuredSizes.mIconBitmap = Utils.createBitmap(nextAlarmIcon);
}
return measuredSizes;
}
/**
* @return "11:59" or "23:59" in the current locale
*/
private static CharSequence getLongestTimeString(TextClock clock) {
final CharSequence format = clock.is24HourModeEnabled()
? clock.getFormat24Hour()
: clock.getFormat12Hour();
final Calendar longestPMTime = Calendar.getInstance();
longestPMTime.set(0, 0, 0, 23, 59);
return DateFormat.format(format, longestPMTime);
}
/**
* @return the locale-specific date pattern
*/
private static String getDateFormat(Context context) {
final Locale locale = Locale.getDefault();
final String skeleton = context.getString(R.string.abbrev_wday_month_day_no_year);
return DateFormat.getBestDateTimePattern(locale, skeleton);
}
/**
* This class stores the target size of the widget as well as the measured size using a given
* clock font size. All other fonts and icons are scaled proportional to the clock font.
@ -474,10 +477,14 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
private int mMeasuredTextClockWidthPx;
private int mMeasuredTextClockHeightPx;
/** The size of the font to use on the date / next alarm time fields. */
/**
* The size of the font to use on the date / next alarm time fields.
*/
private int mFontSizePx;
/** The size of the font to use on the clock field. */
/**
* The size of the font to use on the clock field.
*/
private int mClockFontSizePx;
private int mIconFontSizePx;
@ -490,9 +497,22 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
mSmallestClockFontSizePx = 1;
}
private int getLargestClockFontSizePx() { return mLargestClockFontSizePx; }
private int getSmallestClockFontSizePx() { return mSmallestClockFontSizePx; }
private int getClockFontSizePx() { return mClockFontSizePx; }
private static void append(StringBuilder builder, String format, Object... args) {
builder.append(String.format(Locale.ENGLISH, format, args));
}
private int getLargestClockFontSizePx() {
return mLargestClockFontSizePx;
}
private int getSmallestClockFontSizePx() {
return mSmallestClockFontSizePx;
}
private int getClockFontSizePx() {
return mClockFontSizePx;
}
private void setClockFontSizePx(int clockFontSizePx) {
mClockFontSizePx = clockFontSizePx;
mFontSizePx = max(1, round(clockFontSizePx / 7.5f));
@ -515,6 +535,7 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
return new Sizes(mTargetWidthPx, mTargetHeightPx, mLargestClockFontSizePx);
}
@NonNull
@Override
public String toString() {
final StringBuilder builder = new StringBuilder(1000);
@ -535,9 +556,5 @@ public class DigitalAppWidgetProvider extends AppWidgetProvider {
append(builder, "Clock font: %dpx\n", mClockFontSizePx);
return builder.toString();
}
private static void append(StringBuilder builder, String format, Object... args) {
builder.append(String.format(Locale.ENGLISH, format, args));
}
}
}

View file

@ -26,7 +26,8 @@ import com.best.deskclock.Utils;
public final class WidgetUtils {
private WidgetUtils() {}
private WidgetUtils() {
}
// Calculate the scale factor of the fonts in the widget
public static float getScaleRatio(Context context, Bundle options, int id, int cityCount) {
@ -51,14 +52,13 @@ public final class WidgetUtils {
ratio *= .83f;
if (cityCount > 0) {
return (ratio > 1f) ? 1f : ratio;
return Math.min(ratio, 1f);
}
ratio = Math.min(ratio, 1.6f);
if (Utils.isPortrait(context)) {
ratio = Math.max(ratio, .71f);
}
else {
} else {
ratio = Math.max(ratio, .45f);
}
return ratio;

View file

@ -16,6 +16,8 @@
package com.best.deskclock;
import static com.best.deskclock.uidata.UiDataModel.Tab.ALARMS;
import android.app.LoaderManager;
import android.content.Context;
import android.content.Intent;
@ -24,40 +26,38 @@ import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import com.google.android.material.snackbar.Snackbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.best.deskclock.alarms.AlarmTimeClickHandler;
import com.best.deskclock.alarms.AlarmUpdateHandler;
import com.best.deskclock.alarms.ScrollHandler;
import com.best.deskclock.alarms.dataadapter.AlarmItemViewHolder;
import com.best.deskclock.alarms.TimePickerDialogFragment;
import com.best.deskclock.alarms.dataadapter.AlarmItemHolder;
import com.best.deskclock.events.Events;
import com.best.deskclock.alarms.dataadapter.AlarmItemViewHolder;
import com.best.deskclock.alarms.dataadapter.CollapsedAlarmViewHolder;
import com.best.deskclock.alarms.dataadapter.ExpandedAlarmViewHolder;
import com.best.deskclock.events.Events;
import com.best.deskclock.provider.Alarm;
import com.best.deskclock.provider.AlarmInstance;
import com.best.deskclock.uidata.UiDataModel;
import com.best.deskclock.widget.EmptyViewController;
import com.best.deskclock.widget.toast.SnackbarManager;
import com.best.deskclock.widget.toast.ToastManager;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.List;
import static com.best.deskclock.uidata.UiDataModel.Tab.ALARMS;
import java.util.Objects;
/**
* A fragment that displays a list of alarm time and allows interaction with them.
@ -119,7 +119,7 @@ public final class AlarmClockFragment extends DeskClockFragment implements
final View v = inflater.inflate(R.layout.alarm_clock, container, false);
final Context context = getActivity();
mRecyclerView = (RecyclerView) v.findViewById(R.id.alarms_recycler_view);
mRecyclerView = v.findViewById(R.id.alarms_recycler_view);
mLayoutManager = new LinearLayoutManager(context) {
@Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
@ -131,9 +131,9 @@ public final class AlarmClockFragment extends DeskClockFragment implements
}
};
mRecyclerView.setLayoutManager(mLayoutManager);
mMainLayout = (ViewGroup) v.findViewById(R.id.main);
mMainLayout = v.findViewById(R.id.main);
mAlarmUpdateHandler = new AlarmUpdateHandler(context, this, mMainLayout);
final TextView emptyView = (TextView) v.findViewById(R.id.alarms_empty_view);
final TextView emptyView = v.findViewById(R.id.alarms_empty_view);
final Drawable noAlarms = Utils.getVectorDrawable(context, R.drawable.ic_noalarms);
emptyView.setCompoundDrawablesWithIntrinsicBounds(null, noAlarms, null, null);
mEmptyViewController = new EmptyViewController(mMainLayout, mRecyclerView, emptyView);
@ -161,7 +161,7 @@ public final class AlarmClockFragment extends DeskClockFragment implements
final RecyclerView.ViewHolder viewHolder =
mRecyclerView.findViewHolderForItemId(mExpandedAlarmId);
if (viewHolder != null) {
smoothScrollTo(viewHolder.getAdapterPosition());
smoothScrollTo(viewHolder.getBindingAdapterPosition());
}
}
} else if (mExpandedAlarmId == holder.itemId) {
@ -183,15 +183,15 @@ public final class AlarmClockFragment extends DeskClockFragment implements
itemAnimator.setChangeDuration(300L);
itemAnimator.setMoveDuration(300L);
mRecyclerView.setItemAnimator(itemAnimator);
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
AlarmItemViewHolder alarmHolder = (AlarmItemViewHolder) viewHolder;
AlarmItemHolder itemHolder = alarmHolder.getItemHolder();
@ -201,11 +201,11 @@ public final class AlarmClockFragment extends DeskClockFragment implements
mAlarmUpdateHandler.asyncDeleteAlarm(alarm);
}
}).attachToRecyclerView(mRecyclerView);
return v;
return v;
}
@Override
public void onStart() {
super.onStart();
@ -324,15 +324,15 @@ public final class AlarmClockFragment extends DeskClockFragment implements
return;
}
if (mRecyclerView.getItemAnimator().isRunning()) {
if (Objects.requireNonNull(mRecyclerView.getItemAnimator()).isRunning()) {
// RecyclerView is currently animating -> defer update.
mRecyclerView.getItemAnimator().isRunning(
new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
@Override
public void onAnimationsFinished() {
setAdapterItems(items, updateToken);
}
});
@Override
public void onAnimationsFinished() {
setAdapterItems(items, updateToken);
}
});
} else if (mRecyclerView.isComputingLayout()) {
// RecyclerView is currently computing a layout -> defer update.
mRecyclerView.post(new Runnable() {
@ -427,12 +427,13 @@ public final class AlarmClockFragment extends DeskClockFragment implements
}
private void startCreatingAlarm() {
public void startCreatingAlarm() {
// Clear the currently selected alarm.
mAlarmTimeClickHandler.setSelectedAlarm(null);
TimePickerDialogFragment.show(this);
}
@Override
public void onTimeSet(TimePickerDialogFragment fragment, int hourOfDay, int minute) {
mAlarmTimeClickHandler.onTimeSet(hourOfDay, minute);
@ -449,13 +450,13 @@ public final class AlarmClockFragment extends DeskClockFragment implements
private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
implements View.OnLayoutChangeListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView));
}
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
int oldLeft, int oldTop, int oldRight, int oldBottom) {
setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView));
}
}

View file

@ -23,11 +23,10 @@ import android.content.Context;
import android.content.Intent;
import android.os.PowerManager.WakeLock;
import com.best.deskclock.alarms.AlarmStateManager;
import com.best.deskclock.alarms.AlarmNotifications;
import com.best.deskclock.alarms.AlarmStateManager;
import com.best.deskclock.controller.Controller;
import com.best.deskclock.data.DataModel;
import com.best.deskclock.NotificationUtils;
import com.best.deskclock.provider.AlarmInstance;
import java.util.Calendar;

View file

@ -17,14 +17,18 @@
package com.best.deskclock;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Objects;
/**
* Thin wrapper around RecyclerView to prevent simultaneous layout passes, particularly during
* animations.
* Thin wrapper around RecyclerView to prevent simultaneous layout passes, particularly during
* animations.
*/
public class AlarmRecyclerView extends RecyclerView {
@ -42,9 +46,9 @@ public class AlarmRecyclerView extends RecyclerView {
super(context, attrs, defStyle);
addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
// Disable scrolling/user action to prevent choppy animations.
return rv.getItemAnimator().isRunning();
return Objects.requireNonNull(rv.getItemAnimator()).isRunning();
}
});
}

View file

@ -35,15 +35,16 @@ import java.util.Locale;
public class AlarmSelectionActivity extends ListActivity {
/** Used by default when an invalid action provided. */
private static final int ACTION_INVALID = -1;
/** Action used to signify alarm should be dismissed on selection. */
/**
* Action used to signify alarm should be dismissed on selection.
*/
public static final int ACTION_DISMISS = 0;
public static final String EXTRA_ACTION = "com.best.deskclock.EXTRA_ACTION";
public static final String EXTRA_ALARMS = "com.best.deskclock.EXTRA_ALARMS";
/**
* Used by default when an invalid action provided.
*/
private static final int ACTION_INVALID = -1;
private final List<AlarmSelection> mSelections = new ArrayList<>();
private int mAction;
@ -61,7 +62,7 @@ public class AlarmSelectionActivity extends ListActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.selection_layout);
final Button cancelButton = (Button) findViewById(R.id.cancel_button);
final Button cancelButton = findViewById(R.id.cancel_button);
cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

View file

@ -17,16 +17,17 @@
package com.best.deskclock;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import com.google.android.material.snackbar.Snackbar;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.VisibleForTesting;
import com.best.deskclock.provider.AlarmInstance;
import com.best.deskclock.widget.toast.SnackbarManager;
import com.best.deskclock.widget.toast.ToastManager;
import com.google.android.material.snackbar.Snackbar;
import java.util.Calendar;
import java.util.Locale;
@ -49,7 +50,7 @@ public class AlarmUtils {
}
public static String getAlarmText(Context context, AlarmInstance instance,
boolean includeLabel) {
boolean includeLabel) {
String alarmTimeStr = getFormattedTime(context, instance.getAlarmTime());
return (instance.mLabel.isEmpty() || !includeLabel)
? alarmTimeStr

View file

@ -16,29 +16,36 @@
package com.best.deskclock;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.appcompat.widget.AppCompatImageView;
import android.graphics.Color;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.appcompat.widget.AppCompatImageView;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
/**
* This widget display an analog clock with two hands for hours and minutes.
*/
public class AnalogClock extends FrameLayout {
private final ImageView mHourHand;
private final ImageView mMinuteHand;
private final ImageView mSecondHand;
private final String mDescFormat;
private Calendar mTime;
private TimeZone mTimeZone;
private boolean mEnableSeconds = true;
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -49,7 +56,6 @@ public class AnalogClock extends FrameLayout {
onTimeChanged();
}
};
private final Runnable mClockTick = new Runnable() {
@Override
public void run() {
@ -63,15 +69,6 @@ public class AnalogClock extends FrameLayout {
}
};
private final ImageView mHourHand;
private final ImageView mMinuteHand;
private final ImageView mSecondHand;
private Calendar mTime;
private String mDescFormat;
private TimeZone mTimeZone;
private boolean mEnableSeconds = true;
public AnalogClock(Context context) {
this(context, null /* attrs */);
}
@ -108,7 +105,7 @@ public class AnalogClock extends FrameLayout {
mSecondHand.getDrawable().mutate();
addView(mSecondHand);
if (context.getClass().getSimpleName().equalsIgnoreCase(ScreensaverActivity.class.getSimpleName())){
if (context.getClass().getSimpleName().equalsIgnoreCase(ScreensaverActivity.class.getSimpleName())) {
dial.setColorFilter(Color.WHITE);
mHourHand.setColorFilter(Color.WHITE);
mMinuteHand.setColorFilter(Color.WHITE);

View file

@ -26,13 +26,14 @@ import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import android.util.Property;
import android.view.View;
import android.view.animation.Interpolator;
import android.widget.ImageView;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -50,28 +51,111 @@ public class AnimatorUtils {
public static final Property<View, Integer> BACKGROUND_ALPHA =
new Property<View, Integer>(Integer.class, "background.alpha") {
@Override
public Integer get(View view) {
Drawable background = view.getBackground();
if (background instanceof LayerDrawable
&& ((LayerDrawable) background).getNumberOfLayers() > 0) {
background = ((LayerDrawable) background).getDrawable(0);
}
return background.getAlpha();
}
@Override
public Integer get(View view) {
Drawable background = view.getBackground();
if (background instanceof LayerDrawable
&& ((LayerDrawable) background).getNumberOfLayers() > 0) {
background = ((LayerDrawable) background).getDrawable(0);
}
return background.getAlpha();
}
@Override
public void set(View view, Integer value) {
setBackgroundAlpha(view, value);
}
};
@Override
public void set(View view, Integer value) {
setBackgroundAlpha(view, value);
}
};
public static final Property<ImageView, Integer> DRAWABLE_ALPHA =
new Property<ImageView, Integer>(Integer.class, "drawable.alpha") {
@Override
public Integer get(ImageView view) {
return view.getDrawable().getAlpha();
}
@Override
public void set(ImageView view, Integer value) {
view.getDrawable().setAlpha(value);
}
};
public static final Property<ImageView, Integer> DRAWABLE_TINT =
new Property<ImageView, Integer>(Integer.class, "drawable.tint") {
@Override
public Integer get(ImageView view) {
return null;
}
@Override
public void set(ImageView view, Integer value) {
// Ensure the drawable is wrapped using DrawableCompat.
final Drawable drawable = view.getDrawable();
final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
if (wrappedDrawable != drawable) {
view.setImageDrawable(wrappedDrawable);
}
// Set the new tint value via DrawableCompat.
DrawableCompat.setTint(wrappedDrawable, value);
}
};
@SuppressWarnings("unchecked")
public static final TypeEvaluator<Integer> ARGB_EVALUATOR = new ArgbEvaluator();
public static final Property<View, Integer> VIEW_LEFT =
new Property<View, Integer>(Integer.class, "left") {
@Override
public Integer get(View view) {
return view.getLeft();
}
@Override
public void set(View view, Integer left) {
view.setLeft(left);
}
};
public static final Property<View, Integer> VIEW_TOP =
new Property<View, Integer>(Integer.class, "top") {
@Override
public Integer get(View view) {
return view.getTop();
}
@Override
public void set(View view, Integer top) {
view.setTop(top);
}
};
public static final Property<View, Integer> VIEW_BOTTOM =
new Property<View, Integer>(Integer.class, "bottom") {
@Override
public Integer get(View view) {
return view.getBottom();
}
@Override
public void set(View view, Integer bottom) {
view.setBottom(bottom);
}
};
public static final Property<View, Integer> VIEW_RIGHT =
new Property<View, Integer>(Integer.class, "right") {
@Override
public Integer get(View view) {
return view.getRight();
}
@Override
public void set(View view, Integer right) {
view.setRight(right);
}
};
private static Method sAnimateValue;
private static boolean sTryAnimateValue = true;
/**
* Sets the alpha of the top layer's drawable (of the background) only, if the background is a
* layer drawable, to ensure that the other layers (i.e., the selectable item background, and
* therefore the touch feedback RippleDrawable) are not affected.
*
* @param view the affected view
* @param view the affected view
* @param value the alpha value (0-255)
*/
public static void setBackgroundAlpha(View view, Integer value) {
@ -83,45 +167,6 @@ public class AnimatorUtils {
background.setAlpha(value);
}
public static final Property<ImageView, Integer> DRAWABLE_ALPHA =
new Property<ImageView, Integer>(Integer.class, "drawable.alpha") {
@Override
public Integer get(ImageView view) {
return view.getDrawable().getAlpha();
}
@Override
public void set(ImageView view, Integer value) {
view.getDrawable().setAlpha(value);
}
};
public static final Property<ImageView, Integer> DRAWABLE_TINT =
new Property<ImageView, Integer>(Integer.class, "drawable.tint") {
@Override
public Integer get(ImageView view) {
return null;
}
@Override
public void set(ImageView view, Integer value) {
// Ensure the drawable is wrapped using DrawableCompat.
final Drawable drawable = view.getDrawable();
final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
if (wrappedDrawable != drawable) {
view.setImageDrawable(wrappedDrawable);
}
// Set the new tint value via DrawableCompat.
DrawableCompat.setTint(wrappedDrawable, value);
}
};
@SuppressWarnings("unchecked")
public static final TypeEvaluator<Integer> ARGB_EVALUATOR = new ArgbEvaluator();
private static Method sAnimateValue;
private static boolean sTryAnimateValue = true;
public static void setAnimatedFraction(ValueAnimator animator, float fraction) {
if (Utils.isLMR1OrLater()) {
animator.setCurrentFraction(fraction);
@ -178,66 +223,14 @@ public class AnimatorUtils {
return ObjectAnimator.ofFloat(view, View.ALPHA, values);
}
public static final Property<View, Integer> VIEW_LEFT =
new Property<View, Integer>(Integer.class, "left") {
@Override
public Integer get(View view) {
return view.getLeft();
}
@Override
public void set(View view, Integer left) {
view.setLeft(left);
}
};
public static final Property<View, Integer> VIEW_TOP =
new Property<View, Integer>(Integer.class, "top") {
@Override
public Integer get(View view) {
return view.getTop();
}
@Override
public void set(View view, Integer top) {
view.setTop(top);
}
};
public static final Property<View, Integer> VIEW_BOTTOM =
new Property<View, Integer>(Integer.class, "bottom") {
@Override
public Integer get(View view) {
return view.getBottom();
}
@Override
public void set(View view, Integer bottom) {
view.setBottom(bottom);
}
};
public static final Property<View, Integer> VIEW_RIGHT =
new Property<View, Integer>(Integer.class, "right") {
@Override
public Integer get(View view) {
return view.getRight();
}
@Override
public void set(View view, Integer right) {
view.setRight(right);
}
};
/**
* @param target the view to be morphed
* @param from the bounds of the {@code target} before animating
* @param to the bounds of the {@code target} after animating
* @param from the bounds of the {@code target} before animating
* @param to the bounds of the {@code target} after animating
* @return an animator that morphs the {@code target} between the {@code from} bounds and the
* {@code to} bounds. Note that it is the *content* bounds that matter here, so padding
* insets contributed by the background are subtracted from the views when computing the
* {@code target} bounds.
* {@code to} bounds. Note that it is the *content* bounds that matter here, so padding
* insets contributed by the background are subtracted from the views when computing the
* {@code target} bounds.
*/
public static Animator getBoundsAnimator(View target, View from, View to) {
// Fetch the content insets for the views. Content bounds are what matter, not total bounds.
@ -268,7 +261,7 @@ public class AnimatorUtils {
* Returns an animator that animates the bounds of a single view.
*/
public static Animator getBoundsAnimator(View view, int fromLeft, int fromTop, int fromRight,
int fromBottom, int toLeft, int toTop, int toRight, int toBottom) {
int fromBottom, int toLeft, int toTop, int toRight, int toBottom) {
view.setLeft(fromLeft);
view.setTop(fromTop);
view.setRight(fromRight);

View file

@ -32,9 +32,10 @@ public final class AsyncHandler {
sHandler = new Handler(sHandlerThread.getLooper());
}
private AsyncHandler() {
}
public static void post(Runnable r) {
sHandler.post(r);
}
private AsyncHandler() {}
}

View file

@ -1,5 +1,8 @@
package com.best.deskclock;
import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
import static android.media.AudioManager.STREAM_ALARM;
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.AudioAttributes;
@ -18,9 +21,6 @@ import android.telephony.TelephonyManager;
import java.io.IOException;
import java.lang.reflect.Method;
import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
import static android.media.AudioManager.STREAM_ALARM;
/**
* <p>This class controls playback of ringtones. Uses {@link Ringtone} or {@link MediaPlayer} in a
* dedicated thread so that this class can be called from the main thread. Consequently, problems
@ -60,33 +60,88 @@ public final class AsyncRingtonePlayer {
private static final int EVENT_VOLUME = 3;
private static final String RINGTONE_URI_KEY = "RINGTONE_URI_KEY";
private static final String CRESCENDO_DURATION_KEY = "CRESCENDO_DURATION_KEY";
/** Handler running on the ringtone thread. */
private Handler mHandler;
/** {@link MediaPlayerPlaybackDelegate} on pre M; {@link RingtonePlaybackDelegate} on M+ */
private PlaybackDelegate mPlaybackDelegate;
/** The context. */
/**
* The context.
*/
private final Context mContext;
/**
* Handler running on the ringtone thread.
*/
private Handler mHandler;
/**
* {@link MediaPlayerPlaybackDelegate} on pre M; {@link RingtonePlaybackDelegate} on M+
*/
private PlaybackDelegate mPlaybackDelegate;
public AsyncRingtonePlayer(Context context) {
mContext = context;
}
/** Plays the ringtone. */
/**
* @return <code>true</code> iff the device is currently in a telephone call
*/
private static boolean isInTelephoneCall(Context context) {
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
}
/**
* @return Uri of the ringtone to play when the user is in a telephone call
*/
private static Uri getInCallRingtoneUri(Context context) {
return Utils.getResourceUri(context, R.raw.alarm_expire);
}
/**
* @return Uri of the ringtone to play when the chosen ringtone fails to play
*/
private static Uri getFallbackRingtoneUri(Context context) {
return Utils.getResourceUri(context, R.raw.alarm_expire);
}
/**
* @param currentTime current time of the device
* @param stopTime time at which the crescendo finishes
* @param duration length of time over which the crescendo occurs
* @return the scalar volume value that produces a linear increase in volume (in decibels)
*/
private static float computeVolume(long currentTime, long stopTime, long duration) {
// Compute the percentage of the crescendo that has completed.
final float elapsedCrescendoTime = stopTime - currentTime;
final float fractionComplete = 1 - (elapsedCrescendoTime / duration);
// Use the fraction to compute a target decibel between -40dB (near silent) and 0dB (max).
final float gain = (fractionComplete * 40) - 40;
// Convert the target gain (in decibels) into the corresponding volume scalar.
final float volume = (float) Math.pow(10f, gain / 20f);
LOGGER.v("Ringtone crescendo %,.2f%% complete (scalar: %f, volume: %f dB)",
fractionComplete * 100, volume, gain);
return volume;
}
/**
* Plays the ringtone.
*/
public void play(Uri ringtoneUri, long crescendoDuration) {
LOGGER.d("Posting play.");
postMessage(EVENT_PLAY, ringtoneUri, crescendoDuration, 0);
}
/** Stops playing the ringtone. */
/**
* Stops playing the ringtone.
*/
public void stop() {
LOGGER.d("Posting stop.");
postMessage(EVENT_STOP, null, 0, 0);
}
/** Schedules an adjustment of the playback volume 50ms in the future. */
/**
* Schedules an adjustment of the playback volume 50ms in the future.
*/
private void scheduleVolumeAdjustment() {
LOGGER.v("Adjusting volume.");
@ -100,13 +155,13 @@ public final class AsyncRingtonePlayer {
/**
* Posts a message to the ringtone-thread handler.
*
* @param messageCode the message to post
* @param ringtoneUri the ringtone in question, if any
* @param messageCode the message to post
* @param ringtoneUri the ringtone in question, if any
* @param crescendoDuration the length of time, in ms, over which to crescendo the ringtone
* @param delayMillis the amount of time to delay sending the message, if any
* @param delayMillis the amount of time to delay sending the message, if any
*/
private void postMessage(int messageCode, Uri ringtoneUri, long crescendoDuration,
long delayMillis) {
long delayMillis) {
synchronized (this) {
if (mHandler == null) {
mHandler = getNewHandler();
@ -157,29 +212,6 @@ public final class AsyncRingtonePlayer {
};
}
/**
* @return <code>true</code> iff the device is currently in a telephone call
*/
private static boolean isInTelephoneCall(Context context) {
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
}
/**
* @return Uri of the ringtone to play when the user is in a telephone call
*/
private static Uri getInCallRingtoneUri(Context context) {
return Utils.getResourceUri(context, R.raw.alarm_expire);
}
/**
* @return Uri of the ringtone to play when the chosen ringtone fails to play
*/
private static Uri getFallbackRingtoneUri(Context context) {
return Utils.getResourceUri(context, R.raw.alarm_expire);
}
/**
* Check if the executing thread is the one dedicated to controlling the ringtone playback.
*/
@ -190,29 +222,6 @@ public final class AsyncRingtonePlayer {
}
}
/**
* @param currentTime current time of the device
* @param stopTime time at which the crescendo finishes
* @param duration length of time over which the crescendo occurs
* @return the scalar volume value that produces a linear increase in volume (in decibels)
*/
private static float computeVolume(long currentTime, long stopTime, long duration) {
// Compute the percentage of the crescendo that has completed.
final float elapsedCrescendoTime = stopTime - currentTime;
final float fractionComplete = 1 - (elapsedCrescendoTime / duration);
// Use the fraction to compute a target decibel between -40dB (near silent) and 0dB (max).
final float gain = (fractionComplete * 40) - 40;
// Convert the target gain (in decibels) into the corresponding volume scalar.
final float volume = (float) Math.pow(10f, gain/20f);
LOGGER.v("Ringtone crescendo %,.2f%% complete (scalar: %f, volume: %f dB)",
fractionComplete * 100, volume, gain);
return volume;
}
/**
* @return the platform-specific playback delegate to use to play the ringtone
*/
@ -260,16 +269,24 @@ public final class AsyncRingtonePlayer {
*/
private class MediaPlayerPlaybackDelegate implements PlaybackDelegate {
/** The audio focus manager. Only used by the ringtone thread. */
/**
* The audio focus manager. Only used by the ringtone thread.
*/
private AudioManager mAudioManager;
/** Non-{@code null} while playing a ringtone; {@code null} otherwise. */
/**
* Non-{@code null} while playing a ringtone; {@code null} otherwise.
*/
private MediaPlayer mMediaPlayer;
/** The duration over which to increase the volume. */
/**
* The duration over which to increase the volume.
*/
private long mCrescendoDuration = 0;
/** The time at which the crescendo shall cease; 0 if no crescendo is present. */
/**
* The time at which the crescendo shall cease; 0 if no crescendo is present.
*/
private long mCrescendoStopTime = 0;
/**
@ -335,7 +352,7 @@ public final class AsyncRingtonePlayer {
*
* @param inTelephoneCall {@code true} if there is currently an active telephone call
* @return {@code true} if a crescendo has started and future volume adjustments are
* required to advance the crescendo effect
* required to advance the crescendo effect
*/
private boolean startPlayback(boolean inTelephoneCall)
throws IOException {
@ -437,22 +454,34 @@ public final class AsyncRingtonePlayer {
*/
private class RingtonePlaybackDelegate implements PlaybackDelegate {
/** The audio focus manager. Only used by the ringtone thread. */
/**
* The audio focus manager. Only used by the ringtone thread.
*/
private AudioManager mAudioManager;
/** The current ringtone. Only used by the ringtone thread. */
/**
* The current ringtone. Only used by the ringtone thread.
*/
private Ringtone mRingtone;
/** The method to adjust playback volume; cannot be null. */
/**
* The method to adjust playback volume; cannot be null.
*/
private Method mSetVolumeMethod;
/** The method to adjust playback looping; cannot be null. */
/**
* The method to adjust playback looping; cannot be null.
*/
private Method mSetLoopingMethod;
/** The duration over which to increase the volume. */
/**
* The duration over which to increase the volume.
*/
private long mCrescendoDuration = 0;
/** The time at which the crescendo shall cease; 0 if no crescendo is present. */
/**
* The time at which the crescendo shall cease; 0 if no crescendo is present.
*/
private long mCrescendoStopTime = 0;
private RingtonePlaybackDelegate() {
@ -537,7 +566,7 @@ public final class AsyncRingtonePlayer {
*
* @param inTelephoneCall {@code true} if there is currently an active telephone call
* @return {@code true} if a crescendo has started and future volume adjustments are
* required to advance the crescendo effect
* required to advance the crescendo effect
*/
private boolean startPlayback(boolean inTelephoneCall) {
// Indicate the ringtone should be played via the alarm stream.

View file

@ -22,39 +22,38 @@ import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import androidx.annotation.ColorInt;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import static com.best.deskclock.AnimatorUtils.ARGB_EVALUATOR;
/**
* Base activity class that changes the app window's color based on the current hour.
*/
public abstract class BaseActivity extends AppCompatActivity {
/** Sets the app window color on each frame of the {@link #mAppColorAnimator}. */
/**
* Sets the app window color on each frame of the {@link #mAppColorAnimator}.
*/
private final AppColorAnimationListener mAppColorAnimationListener
= new AppColorAnimationListener();
/** The current animator that is changing the app window color or {@code null}. */
/**
* The current animator that is changing the app window color or {@code null}.
*/
private ValueAnimator mAppColorAnimator;
/** Draws the app window's color. */
/**
* Draws the app window's color.
*/
private ColorDrawable mBackground;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Allow the content to layout behind the status and navigation bars.
// getWindow().getDecorView().setSystemUiVisibility(
// View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
// | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
final @ColorInt int color = ThemeUtils.resolveColor(this, android.R.attr.colorBackground);
adjustAppColor(color, false /* animate */);
adjustAppColor(color /* animate */);
}
@Override
@ -63,16 +62,15 @@ public abstract class BaseActivity extends AppCompatActivity {
// Ensure the app window color is up-to-date.
final @ColorInt int color = ThemeUtils.resolveColor(this, android.R.attr.colorBackground);
adjustAppColor(color, false /* animate */);
adjustAppColor(color /* animate */);
}
/**
* Adjusts the current app window color of this activity; animates the change if desired.
*
* @param color the ARGB value to set as the current app window color
* @param animate {@code true} if the change should be animated
* @param color the ARGB value to set as the current app window color
*/
protected void adjustAppColor(@ColorInt int color, boolean animate) {
protected void adjustAppColor(@ColorInt int color) {
// Create and install the drawable that defines the window color.
if (mBackground == null) {
mBackground = new ColorDrawable(color);
@ -86,15 +84,7 @@ public abstract class BaseActivity extends AppCompatActivity {
final @ColorInt int currentColor = mBackground.getColor();
if (currentColor != color) {
if (animate) {
mAppColorAnimator = ValueAnimator.ofObject(ARGB_EVALUATOR, currentColor, color)
.setDuration(3000L);
mAppColorAnimator.addUpdateListener(mAppColorAnimationListener);
mAppColorAnimator.addListener(mAppColorAnimationListener);
mAppColorAnimator.start();
} else {
setAppColor(color);
}
setAppColor(color);
}
}

View file

@ -9,14 +9,14 @@ import android.widget.FrameLayout;
import android.widget.TextView;
/**
* This class adjusts the locations of children buttons and text of this view group by adjusting the
* This class adjusts the locations of child buttons and text of this view group by adjusting the
* margins of each item. The left and right buttons are aligned with the bottom of the circle. The
* stop button and label text are located within the circle with the stop button near the bottom and
* the label text near the top. The maximum text size for the label text view is also calculated.
*/
public class CircleButtonsLayout extends FrameLayout {
private float mDiamOffset;
private final float mDiamOffset;
private View mCircleView;
private Button mResetAddButton;
private TextView mLabel;
@ -31,9 +31,7 @@ public class CircleButtonsLayout extends FrameLayout {
final Resources res = getContext().getResources();
final float strokeSize = res.getDimension(R.dimen.circletimer_circle_size);
final float dotStrokeSize = res.getDimension(R.dimen.circletimer_dot_size);
final float markerStrokeSize = res.getDimension(R.dimen.circletimer_marker_size);
mDiamOffset = Utils.calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize) * 2;
mDiamOffset = strokeSize * 2;
}
@Override
@ -49,8 +47,8 @@ public class CircleButtonsLayout extends FrameLayout {
protected void remeasureViews() {
if (mLabel == null) {
mCircleView = findViewById(R.id.timer_time);
mLabel = (TextView) findViewById(R.id.timer_label);
mResetAddButton = (Button) findViewById(R.id.reset_add);
mLabel = findViewById(R.id.timer_label);
mResetAddButton = findViewById(R.id.reset_add);
}
final int frameWidth = mCircleView.getMeasuredWidth();
@ -69,9 +67,9 @@ public class CircleButtonsLayout extends FrameLayout {
if (mLabel != null) {
MarginLayoutParams labelParams = (MarginLayoutParams) mLabel.getLayoutParams();
labelParams.topMargin = circleDiam/6;
labelParams.topMargin = circleDiam / 6;
if (minBound == frameWidth) {
labelParams.topMargin += (frameHeight-frameWidth)/2;
labelParams.topMargin += (frameHeight - frameWidth) / 2;
}
/* The following formula has been simplified based on the following:
* Our goal is to calculate the maximum width for the label frame.
@ -134,4 +132,4 @@ public class CircleButtonsLayout extends FrameLayout {
mLabel.setMaxWidth((int) w);
}
}
}
}

View file

@ -16,6 +16,13 @@
package com.best.deskclock;
import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static com.best.deskclock.uidata.UiDataModel.Tab.CLOCKS;
import static java.util.Calendar.DAY_OF_WEEK;
import android.app.Activity;
import android.app.AlarmManager;
import android.content.BroadcastReceiver;
@ -28,9 +35,6 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.format.DateUtils;
import android.view.GestureDetector;
import android.view.LayoutInflater;
@ -42,6 +46,10 @@ import android.widget.ImageView;
import android.widget.TextClock;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.best.deskclock.data.City;
import com.best.deskclock.data.CityListener;
import com.best.deskclock.data.DataModel;
@ -53,13 +61,6 @@ import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;
import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static com.best.deskclock.uidata.UiDataModel.Tab.CLOCKS;
import static java.util.Calendar.DAY_OF_WEEK;
/**
* Fragment that shows the clock (analog or digital), the next alarm info and the world clock.
*/
@ -109,7 +110,7 @@ public final class ClockFragment extends DeskClockFragment {
mCityAdapter = new SelectedCitiesAdapter(getActivity(), mDateFormat,
mDateFormatForAccessibility);
mCityList = (RecyclerView) fragmentView.findViewById(R.id.cities);
mCityList = fragmentView.findViewById(R.id.cities);
mCityList.setLayoutManager(new LinearLayoutManager(getActivity()));
mCityList.setAdapter(mCityAdapter);
mCityList.setItemAnimator(null);
@ -126,8 +127,8 @@ public final class ClockFragment extends DeskClockFragment {
// on as a header to the main listview.
mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane);
if (mClockFrame != null) {
mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock);
mAnalogClock = (AnalogClock) mClockFrame.findViewById(R.id.analog_clock);
mDigitalClock = mClockFrame.findViewById(R.id.digital_clock);
mAnalogClock = mClockFrame.findViewById(R.id.analog_clock);
Utils.setClockIconTypeface(mClockFrame);
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame);
Utils.setClockStyle(mDigitalClock, mAnalogClock);
@ -171,7 +172,6 @@ public final class ClockFragment extends DeskClockFragment {
// Alarm observer is null on L or later.
if (mAlarmObserver != null) {
@SuppressWarnings("deprecation")
final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
activity.getContentResolver().registerContentObserver(uri, false, mAlarmObserver);
}
@ -227,6 +227,225 @@ public final class ClockFragment extends DeskClockFragment {
}
}
/**
* This adapter lists all of the selected world clocks. Optionally, it also includes a clock at
* the top for the home timezone if "Automatic home clock" is turned on in settings and the
* current time at home does not match the current time in the timezone of the current location.
* If the phone is in portrait mode it will also include the main clock at the top.
*/
private static final class SelectedCitiesAdapter extends RecyclerView.Adapter
implements CityListener {
private final static int MAIN_CLOCK = R.layout.main_clock_frame;
private final static int WORLD_CLOCK = R.layout.world_clock_item;
private final LayoutInflater mInflater;
private final Context mContext;
private final boolean mIsPortrait;
private final boolean mShowHomeClock;
private final String mDateFormat;
private final String mDateFormatForAccessibility;
private SelectedCitiesAdapter(Context context, String dateFormat,
String dateFormatForAccessibility) {
mContext = context;
mDateFormat = dateFormat;
mDateFormatForAccessibility = dateFormatForAccessibility;
mInflater = LayoutInflater.from(context);
mIsPortrait = Utils.isPortrait(context);
mShowHomeClock = DataModel.getDataModel().getShowHomeClock();
}
@Override
public int getItemViewType(int position) {
if (position == 0 && mIsPortrait) {
return MAIN_CLOCK;
}
return WORLD_CLOCK;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final View view = mInflater.inflate(viewType, parent, false);
switch (viewType) {
case WORLD_CLOCK:
return new CityViewHolder(view);
case MAIN_CLOCK:
return new MainClockViewHolder(view);
default:
throw new IllegalArgumentException("View type not recognized");
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
final int viewType = getItemViewType(position);
switch (viewType) {
case WORLD_CLOCK:
// Retrieve the city to bind.
final City city;
// If showing home clock, put it at the top
if (mShowHomeClock && position == (mIsPortrait ? 1 : 0)) {
city = getHomeCity();
} else {
final int positionAdjuster = (mIsPortrait ? 1 : 0)
+ (mShowHomeClock ? 1 : 0);
city = getCities().get(position - positionAdjuster);
}
((CityViewHolder) holder).bind(mContext, city, position, mIsPortrait);
break;
case MAIN_CLOCK:
((MainClockViewHolder) holder).bind(mContext, mDateFormat,
mDateFormatForAccessibility, getItemCount() > 1);
break;
default:
throw new IllegalArgumentException("Unexpected view type: " + viewType);
}
}
@Override
public int getItemCount() {
final int mainClockCount = mIsPortrait ? 1 : 0;
final int homeClockCount = mShowHomeClock ? 1 : 0;
final int worldClockCount = getCities().size();
return mainClockCount + homeClockCount + worldClockCount;
}
private City getHomeCity() {
return DataModel.getDataModel().getHomeCity();
}
private List<City> getCities() {
return DataModel.getDataModel().getSelectedCities();
}
private void refreshAlarm() {
if (mIsPortrait && getItemCount() > 0) {
notifyItemChanged(0);
}
}
@Override
public void citiesChanged(List<City> oldCities, List<City> newCities) {
notifyDataSetChanged();
}
private static final class CityViewHolder extends RecyclerView.ViewHolder {
private final TextView mName;
private final TextClock mDigitalClock;
private final AnalogClock mAnalogClock;
private final TextView mHoursAhead;
private CityViewHolder(View itemView) {
super(itemView);
mName = itemView.findViewById(R.id.city_name);
mDigitalClock = itemView.findViewById(R.id.digital_clock);
mAnalogClock = itemView.findViewById(R.id.analog_clock);
mHoursAhead = itemView.findViewById(R.id.hours_ahead);
}
private void bind(Context context, City city, int position, boolean isPortrait) {
final String cityTimeZoneId = city.getTimeZone().getID();
// Configure the digital clock or analog clock depending on the user preference.
if (DataModel.getDataModel().getClockStyle() == DataModel.ClockStyle.ANALOG) {
mDigitalClock.setVisibility(GONE);
mAnalogClock.setVisibility(VISIBLE);
mAnalogClock.setTimeZone(cityTimeZoneId);
mAnalogClock.enableSeconds(false);
} else {
mAnalogClock.setVisibility(GONE);
mDigitalClock.setVisibility(VISIBLE);
mDigitalClock.setTimeZone(cityTimeZoneId);
mDigitalClock.setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */,
false));
mDigitalClock.setFormat24Hour(Utils.get24ModeFormat(false));
}
// Supply top and bottom padding dynamically.
final Resources res = context.getResources();
final int padding = res.getDimensionPixelSize(R.dimen.medium_space_top);
final int top = position == 0 && !isPortrait ? 0 : padding;
final int left = itemView.getPaddingLeft();
final int right = itemView.getPaddingRight();
final int bottom = itemView.getPaddingBottom();
itemView.setPadding(left, top, right, bottom);
// Bind the city name.
mName.setText(city.getName());
// Compute if the city week day matches the weekday of the current timezone.
final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
final boolean displayDayOfWeek =
localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
// Compare offset from UTC time on today's date (daylight savings time, etc.)
final TimeZone currentTimeZone = TimeZone.getDefault();
final TimeZone cityTimeZone = TimeZone.getTimeZone(cityTimeZoneId);
final long currentTimeMillis = System.currentTimeMillis();
final long currentUtcOffset = currentTimeZone.getOffset(currentTimeMillis);
final long cityUtcOffset = cityTimeZone.getOffset(currentTimeMillis);
final long offsetDelta = cityUtcOffset - currentUtcOffset;
final int hoursDifferent = (int) (offsetDelta / DateUtils.HOUR_IN_MILLIS);
final int minutesDifferent = (int) (offsetDelta / DateUtils.MINUTE_IN_MILLIS) % 60;
final boolean displayMinutes = offsetDelta % DateUtils.HOUR_IN_MILLIS != 0;
final boolean isAhead = hoursDifferent > 0 || (hoursDifferent == 0
&& minutesDifferent > 0);
if (!Utils.isLandscape(context)) {
// Bind the number of hours ahead or behind, or hide if the time is the same.
final boolean displayDifference = hoursDifferent != 0 || displayMinutes;
mHoursAhead.setVisibility(displayDifference ? VISIBLE : GONE);
final String timeString = Utils.createHoursDifferentString(
context, displayMinutes, isAhead, hoursDifferent, minutesDifferent);
mHoursAhead.setText(displayDayOfWeek ?
(context.getString(isAhead ? R.string.world_hours_tomorrow
: R.string.world_hours_yesterday, timeString))
: timeString);
} else {
// Only tomorrow/yesterday should be shown in landscape view.
mHoursAhead.setVisibility(displayDayOfWeek ? View.VISIBLE : View.GONE);
if (displayDayOfWeek) {
mHoursAhead.setText(context.getString(isAhead ? R.string.world_tomorrow
: R.string.world_yesterday));
}
}
}
}
private static final class MainClockViewHolder extends RecyclerView.ViewHolder {
private final View mHairline;
private final TextClock mDigitalClock;
private final AnalogClock mAnalogClock;
private MainClockViewHolder(View itemView) {
super(itemView);
mHairline = itemView.findViewById(R.id.hairline);
mDigitalClock = itemView.findViewById(R.id.digital_clock);
mAnalogClock = itemView.findViewById(R.id.analog_clock);
Utils.setClockIconTypeface(itemView);
}
private void bind(Context context, String dateFormat,
String dateFormatForAccessibility, boolean showHairline) {
Utils.refreshAlarm(context, itemView);
Utils.updateDate(dateFormat, dateFormatForAccessibility, itemView);
Utils.setClockStyle(mDigitalClock, mAnalogClock);
mHairline.setVisibility(showHairline ? VISIBLE : GONE);
Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
}
}
}
/**
* Long pressing over the main clock starts the screen saver.
*/
@ -317,232 +536,14 @@ public final class ClockFragment extends DeskClockFragment {
private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
implements View.OnLayoutChangeListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
setTabScrolledToTop(Utils.isScrolledToTop(mCityList));
}
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
int oldLeft, int oldTop, int oldRight, int oldBottom) {
setTabScrolledToTop(Utils.isScrolledToTop(mCityList));
}
}
/**
* This adapter lists all of the selected world clocks. Optionally, it also includes a clock at
* the top for the home timezone if "Automatic home clock" is turned on in settings and the
* current time at home does not match the current time in the timezone of the current location.
* If the phone is in portrait mode it will also include the main clock at the top.
*/
private static final class SelectedCitiesAdapter extends RecyclerView.Adapter
implements CityListener {
private final static int MAIN_CLOCK = R.layout.main_clock_frame;
private final static int WORLD_CLOCK = R.layout.world_clock_item;
private final LayoutInflater mInflater;
private final Context mContext;
private final boolean mIsPortrait;
private final boolean mShowHomeClock;
private final String mDateFormat;
private final String mDateFormatForAccessibility;
private SelectedCitiesAdapter(Context context, String dateFormat,
String dateFormatForAccessibility) {
mContext = context;
mDateFormat = dateFormat;
mDateFormatForAccessibility = dateFormatForAccessibility;
mInflater = LayoutInflater.from(context);
mIsPortrait = Utils.isPortrait(context);
mShowHomeClock = DataModel.getDataModel().getShowHomeClock();
}
@Override
public int getItemViewType(int position) {
if (position == 0 && mIsPortrait) {
return MAIN_CLOCK;
}
return WORLD_CLOCK;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = mInflater.inflate(viewType, parent, false);
switch (viewType) {
case WORLD_CLOCK:
return new CityViewHolder(view);
case MAIN_CLOCK:
return new MainClockViewHolder(view);
default:
throw new IllegalArgumentException("View type not recognized");
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final int viewType = getItemViewType(position);
switch (viewType) {
case WORLD_CLOCK:
// Retrieve the city to bind.
final City city;
// If showing home clock, put it at the top
if (mShowHomeClock && position == (mIsPortrait ? 1 : 0)) {
city = getHomeCity();
} else {
final int positionAdjuster = (mIsPortrait ? 1 : 0)
+ (mShowHomeClock ? 1 : 0);
city = getCities().get(position - positionAdjuster);
}
((CityViewHolder) holder).bind(mContext, city, position, mIsPortrait);
break;
case MAIN_CLOCK:
((MainClockViewHolder) holder).bind(mContext, mDateFormat,
mDateFormatForAccessibility, getItemCount() > 1);
break;
default:
throw new IllegalArgumentException("Unexpected view type: " + viewType);
}
}
@Override
public int getItemCount() {
final int mainClockCount = mIsPortrait ? 1 : 0;
final int homeClockCount = mShowHomeClock ? 1 : 0;
final int worldClockCount = getCities().size();
return mainClockCount + homeClockCount + worldClockCount;
}
private City getHomeCity() {
return DataModel.getDataModel().getHomeCity();
}
private List<City> getCities() {
return DataModel.getDataModel().getSelectedCities();
}
private void refreshAlarm() {
if (mIsPortrait && getItemCount() > 0) {
notifyItemChanged(0);
}
}
@Override
public void citiesChanged(List<City> oldCities, List<City> newCities) {
notifyDataSetChanged();
}
private static final class CityViewHolder extends RecyclerView.ViewHolder {
private final TextView mName;
private final TextClock mDigitalClock;
private final AnalogClock mAnalogClock;
private final TextView mHoursAhead;
private CityViewHolder(View itemView) {
super(itemView);
mName = (TextView) itemView.findViewById(R.id.city_name);
mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock);
mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock);
mHoursAhead = (TextView) itemView.findViewById(R.id.hours_ahead);
}
private void bind(Context context, City city, int position, boolean isPortrait) {
final String cityTimeZoneId = city.getTimeZone().getID();
// Configure the digital clock or analog clock depending on the user preference.
if (DataModel.getDataModel().getClockStyle() == DataModel.ClockStyle.ANALOG) {
mDigitalClock.setVisibility(GONE);
mAnalogClock.setVisibility(VISIBLE);
mAnalogClock.setTimeZone(cityTimeZoneId);
mAnalogClock.enableSeconds(false);
} else {
mAnalogClock.setVisibility(GONE);
mDigitalClock.setVisibility(VISIBLE);
mDigitalClock.setTimeZone(cityTimeZoneId);
mDigitalClock.setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */,
false));
mDigitalClock.setFormat24Hour(Utils.get24ModeFormat(false));
}
// Supply top and bottom padding dynamically.
final Resources res = context.getResources();
final int padding = res.getDimensionPixelSize(R.dimen.medium_space_top);
final int top = position == 0 && !isPortrait ? 0 : padding;
final int left = itemView.getPaddingLeft();
final int right = itemView.getPaddingRight();
final int bottom = itemView.getPaddingBottom();
itemView.setPadding(left, top, right, bottom);
// Bind the city name.
mName.setText(city.getName());
// Compute if the city week day matches the weekday of the current timezone.
final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
final boolean displayDayOfWeek =
localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
// Compare offset from UTC time on today's date (daylight savings time, etc.)
final TimeZone currentTimeZone = TimeZone.getDefault();
final TimeZone cityTimeZone = TimeZone.getTimeZone(cityTimeZoneId);
final long currentTimeMillis = System.currentTimeMillis();
final long currentUtcOffset = currentTimeZone.getOffset(currentTimeMillis);
final long cityUtcOffset = cityTimeZone.getOffset(currentTimeMillis);
final long offsetDelta = cityUtcOffset - currentUtcOffset;
final int hoursDifferent = (int) (offsetDelta / DateUtils.HOUR_IN_MILLIS);
final int minutesDifferent = (int) (offsetDelta / DateUtils.MINUTE_IN_MILLIS) % 60;
final boolean displayMinutes = offsetDelta % DateUtils.HOUR_IN_MILLIS != 0;
final boolean isAhead = hoursDifferent > 0 || (hoursDifferent == 0
&& minutesDifferent > 0);
if (!Utils.isLandscape(context)) {
// Bind the number of hours ahead or behind, or hide if the time is the same.
final boolean displayDifference = hoursDifferent != 0 || displayMinutes;
mHoursAhead.setVisibility(displayDifference ? VISIBLE : GONE);
final String timeString = Utils.createHoursDifferentString(
context, displayMinutes, isAhead, hoursDifferent, minutesDifferent);
mHoursAhead.setText(displayDayOfWeek ?
(context.getString(isAhead ? R.string.world_hours_tomorrow
: R.string.world_hours_yesterday, timeString))
: timeString);
} else {
// Only tomorrow/yesterday should be shown in landscape view.
mHoursAhead.setVisibility(displayDayOfWeek ? View.VISIBLE : View.GONE);
if (displayDayOfWeek) {
mHoursAhead.setText(context.getString(isAhead ? R.string.world_tomorrow
: R.string.world_yesterday));
}
}
}
}
private static final class MainClockViewHolder extends RecyclerView.ViewHolder {
private final View mHairline;
private final TextClock mDigitalClock;
private final AnalogClock mAnalogClock;
private MainClockViewHolder(View itemView) {
super(itemView);
mHairline = itemView.findViewById(R.id.hairline);
mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock);
mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock);
Utils.setClockIconTypeface(itemView);
}
private void bind(Context context, String dateFormat,
String dateFormatForAccessibility, boolean showHairline) {
Utils.refreshAlarm(context, itemView);
Utils.updateDate(dateFormat, dateFormatForAccessibility, itemView);
Utils.setClockStyle(mDigitalClock, mAnalogClock);
mHairline.setVisibility(showHairline ? VISIBLE : GONE);
Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
}
}
}
}

View file

@ -17,20 +17,25 @@
package com.best.deskclock;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING;
import static com.best.deskclock.AnimatorUtils.getScaleAnimator;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.Fragment;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
@ -48,97 +53,134 @@ import com.best.deskclock.actionbarmenu.OptionsMenuManager;
import com.best.deskclock.actionbarmenu.SettingsMenuItemController;
import com.best.deskclock.data.DataModel;
import com.best.deskclock.data.DataModel.SilentSetting;
import com.best.deskclock.data.DataModel.ThemeButtonBehavior;
import com.best.deskclock.data.OnSilentSettingsListener;
import com.best.deskclock.events.Events;
import com.best.deskclock.LogUtils;
import com.best.deskclock.provider.Alarm;
import com.best.deskclock.uidata.TabListener;
import com.best.deskclock.uidata.UiDataModel;
import com.best.deskclock.widget.toast.SnackbarManager;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.navigation.NavigationBarView;
import com.google.android.material.snackbar.Snackbar;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static com.best.deskclock.AnimatorUtils.getScaleAnimator;
/**
* The main activity of the application which displays 4 different tabs contains alarms, world
* clocks, timers and a stopwatch.
*/
public class DeskClock extends BaseActivity
implements FabContainer, LabelDialogFragment.AlarmLabelDialogHandler {
/** Models the interesting state of display the {@link #mFab} button may inhabit. */
private enum FabState { SHOWING, HIDE_ARMED, HIDING }
/** Coordinates handling of context menu items. */
private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
/** Shrinks the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to nothing. */
private final AnimatorSet mHideAnimation = new AnimatorSet();
/** Grows the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to natural sizes. */
private final AnimatorSet mShowAnimation = new AnimatorSet();
/** Hides, updates, and shows only the {@link #mFab}; the buttons are untouched. */
private final AnimatorSet mUpdateFabOnlyAnimation = new AnimatorSet();
/** Hides, updates, and shows only the {@link #mLeftButton} and {@link #mRightButton}. */
private final AnimatorSet mUpdateButtonsOnlyAnimation = new AnimatorSet();
/** Automatically starts the {@link #mShowAnimation} after {@link #mHideAnimation} ends. */
private final AnimatorListenerAdapter mAutoStartShowListener = new AutoStartShowListener();
/** Updates the user interface to reflect the selected tab from the backing model. */
private final TabListener mTabChangeWatcher = new TabChangeWatcher();
/** Shows/hides a snackbar explaining which setting is suppressing alarms from firing. */
private final OnSilentSettingsListener mSilentSettingChangeWatcher =
new SilentSettingChangeWatcher();
/** Displays a snackbar explaining why alarms may not fire or may fire silently. */
private Runnable mShowSilentSettingSnackbarRunnable;
/** The view to which snackbar items are anchored. */
private View mSnackbarAnchor;
/** The current display state of the {@link #mFab}. */
private FabState mFabState = FabState.SHOWING;
/** The single floating-action button shared across all tabs in the user interface. */
private ImageView mFab;
/** The button left of the {@link #mFab} shared across all tabs in the user interface. */
private Button mLeftButton;
/** The button right of the {@link #mFab} shared across all tabs in the user interface. */
private Button mRightButton;
/** The ViewPager that pages through the fragments representing the content of the tabs. */
private ViewPager mFragmentTabPager;
/** Generates the fragments that are displayed by the {@link #mFragmentTabPager}. */
private FragmentTabPagerAdapter mFragmentTabPagerAdapter;
/** The view that displays the current tab's title */
private TextView mTitleView;
/** The bottom navigation bar */
private BottomNavigationView mBottomNavigation;
/** {@code true} when a settings change necessitates recreating this activity. */
private boolean mRecreateActivity;
private static final String PERMISSION_POWER_OFF_ALARM =
"org.codeaurora.permission.POWER_OFF_ALARM";
private static final int CODE_FOR_ALARM_PERMISSION = 1;
/**
* Coordinates handling of context menu items.
*/
private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
/**
* Shrinks the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to nothing.
*/
private final AnimatorSet mHideAnimation = new AnimatorSet();
/**
* Grows the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to natural sizes.
*/
private final AnimatorSet mShowAnimation = new AnimatorSet();
/**
* Hides, updates, and shows only the {@link #mFab}; the buttons are untouched.
*/
private final AnimatorSet mUpdateFabOnlyAnimation = new AnimatorSet();
/**
* Hides, updates, and shows only the {@link #mLeftButton} and {@link #mRightButton}.
*/
private final AnimatorSet mUpdateButtonsOnlyAnimation = new AnimatorSet();
/**
* Automatically starts the {@link #mShowAnimation} after {@link #mHideAnimation} ends.
*/
private final AnimatorListenerAdapter mAutoStartShowListener = new AutoStartShowListener();
/**
* Updates the user interface to reflect the selected tab from the backing model.
*/
private final TabListener mTabChangeWatcher = new TabChangeWatcher();
/**
* Shows/hides a snackbar explaining which setting is suppressing alarms from firing.
*/
private final OnSilentSettingsListener mSilentSettingChangeWatcher =
new SilentSettingChangeWatcher();
@SuppressLint("NonConstantResourceId")
private final NavigationBarView.OnItemSelectedListener mNavigationListener
= item -> {
UiDataModel.Tab tab = null;
switch (item.getItemId()) {
case R.id.page_alarm:
tab = UiDataModel.Tab.ALARMS;
break;
case R.id.page_clock:
tab = UiDataModel.Tab.CLOCKS;
break;
case R.id.page_timer:
tab = UiDataModel.Tab.TIMERS;
break;
case R.id.page_stopwatch:
tab = UiDataModel.Tab.STOPWATCH;
break;
}
if (tab != null) {
UiDataModel.getUiDataModel().setSelectedTab(tab);
return true;
}
return false;
};
private ThemeButtonBehavior mThemeBehavior;
/**
* Displays a snackbar explaining why alarms may not fire or may fire silently.
*/
private Runnable mShowSilentSettingSnackbarRunnable;
/**
* The view to which snackbar items are anchored.
*/
private View mSnackbarAnchor;
/**
* The current display state of the {@link #mFab}.
*/
private FabState mFabState = FabState.SHOWING;
/**
* The single floating-action button shared across all tabs in the user interface.
*/
private ImageView mFab;
/**
* The button left of the {@link #mFab} shared across all tabs in the user interface.
*/
private Button mLeftButton;
/**
* The button right of the {@link #mFab} shared across all tabs in the user interface.
*/
private Button mRightButton;
/**
* The ViewPager that pages through the fragments representing the content of the tabs.
*/
private ViewPager mFragmentTabPager;
/**
* Generates the fragments that are displayed by the {@link #mFragmentTabPager}.
*/
private FragmentTabPagerAdapter mFragmentTabPagerAdapter;
/**
* The view that displays the current tab's title
*/
private TextView mTitleView;
/**
* The bottom navigation bar
*/
private BottomNavigationView mBottomNavigation;
/**
* {@code true} when a settings change necessitates recreating this activity.
*/
private boolean mRecreateActivity;
@Override
public void onNewIntent(Intent newIntent) {
super.onNewIntent(newIntent);
@ -149,15 +191,25 @@ public class DeskClock extends BaseActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
mThemeBehavior = DataModel.getDataModel().getThemeButtonBehavior();
if (mThemeBehavior == DataModel.ThemeButtonBehavior.DARK) {
getTheme().applyStyle(R.style.Theme_DeskClock_Dark, true);
}
if (mThemeBehavior == DataModel.ThemeButtonBehavior.LIGHT) {
getTheme().applyStyle(R.style.Theme_DeskClock_Light, true);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.desk_clock);
mSnackbarAnchor = findViewById(R.id.content);
checkPermissions();
// Configure the toolbar.
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
final Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
final ActionBar actionBar = getSupportActionBar();
@ -176,28 +228,13 @@ public class DeskClock extends BaseActivity
onCreateOptionsMenu(toolbar.getMenu());
// Configure the buttons shared by the tabs.
mFab = (ImageView) findViewById(R.id.fab);
mLeftButton = (Button) findViewById(R.id.left_button);
mRightButton = (Button) findViewById(R.id.right_button);
mFab = findViewById(R.id.fab);
mLeftButton = findViewById(R.id.left_button);
mRightButton = findViewById(R.id.right_button);
mFab.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
getSelectedDeskClockFragment().onFabClick(mFab);
}
});
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
getSelectedDeskClockFragment().onLeftButtonClick(mLeftButton);
}
});
mRightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
getSelectedDeskClockFragment().onRightButtonClick(mRightButton);
}
});
mFab.setOnClickListener(view -> getSelectedDeskClockFragment().onFabClick(mFab));
mLeftButton.setOnClickListener(view -> getSelectedDeskClockFragment().onLeftButtonClick(mLeftButton));
mRightButton.setOnClickListener(view -> getSelectedDeskClockFragment().onRightButtonClick(mRightButton));
final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
@ -253,7 +290,7 @@ public class DeskClock extends BaseActivity
// Customize the view pager.
mFragmentTabPagerAdapter = new FragmentTabPagerAdapter(this);
mFragmentTabPager = (ViewPager) findViewById(R.id.desk_clock_pager);
mFragmentTabPager = findViewById(R.id.desk_clock_pager);
// Keep all four tabs to minimize jank.
mFragmentTabPager.setOffscreenPageLimit(3);
// Set Accessibility Delegate to null so view pager doesn't intercept movements and
@ -262,10 +299,10 @@ public class DeskClock extends BaseActivity
// Mirror changes made to the selected page of the view pager into UiDataModel.
mFragmentTabPager.addOnPageChangeListener(new PageChangeWatcher());
mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter);
// Mirror changes made to the selected tab into UiDataModel.
mBottomNavigation = findViewById(R.id.bottom_view);
mBottomNavigation.setOnNavigationItemSelectedListener(mNavigationListener);
mBottomNavigation.setOnItemSelectedListener(mNavigationListener);
// Honor changes to the selected tab from outside entities.
UiDataModel.getUiDataModel().addTabListener(mTabChangeWatcher);
@ -273,45 +310,12 @@ public class DeskClock extends BaseActivity
mTitleView = findViewById(R.id.title_view);
}
private BottomNavigationView.OnNavigationItemSelectedListener mNavigationListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
UiDataModel.Tab tab = null;
switch (item.getItemId()) {
case R.id.page_alarm:
tab = UiDataModel.Tab.ALARMS;
break;
case R.id.page_clock:
tab = UiDataModel.Tab.CLOCKS;
break;
case R.id.page_timer:
tab = UiDataModel.Tab.TIMERS;
break;
case R.id.page_stopwatch:
tab = UiDataModel.Tab.STOPWATCH;
break;
}
if (tab != null) {
UiDataModel.getUiDataModel().setSelectedTab(tab);
return true;
}
return false;
}
};
@Override
protected void onStart() {
DataModel.getDataModel().addSilentSettingsListener(mSilentSettingChangeWatcher);
DataModel.getDataModel().setApplicationInForeground(true);
super.onStart();
}
@ -327,17 +331,12 @@ public class DeskClock extends BaseActivity
protected void onPostResume() {
super.onPostResume();
if (mRecreateActivity) {
if (mRecreateActivity) {
mRecreateActivity = false;
// A runnable must be posted here or the new DeskClock activity will be recreated in a
// paused state, even though it is the foreground activity.
mFragmentTabPager.post(new Runnable() {
@Override
public void run() {
recreate();
}
});
mFragmentTabPager.post(this::recreate);
}
}
@ -392,7 +391,7 @@ public class DeskClock extends BaseActivity
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return getSelectedDeskClockFragment().onKeyDown(keyCode,event)
return getSelectedDeskClockFragment().onKeyDown(keyCode, event)
|| super.onKeyDown(keyCode, event);
}
@ -411,10 +410,8 @@ public class DeskClock extends BaseActivity
f.onMorphFab(mFab);
break;
}
switch (updateType & FAB_REQUEST_FOCUS_MASK) {
case FAB_REQUEST_FOCUS:
mFab.requestFocus();
break;
if ((updateType & FAB_REQUEST_FOCUS_MASK) == FAB_REQUEST_FOCUS) {
mFab.requestFocus();
}
switch (updateType & BUTTONS_ANIMATION_MASK) {
case BUTTONS_IMMEDIATE:
@ -424,11 +421,9 @@ public class DeskClock extends BaseActivity
mUpdateButtonsOnlyAnimation.start();
break;
}
switch (updateType & BUTTONS_DISABLE_MASK) {
case BUTTONS_DISABLE:
mLeftButton.setClickable(false);
mRightButton.setClickable(false);
break;
if ((updateType & BUTTONS_DISABLE_MASK) == BUTTONS_DISABLE) {
mLeftButton.setClickable(false);
mRightButton.setClickable(false);
}
switch (updateType & FAB_AND_BUTTONS_SHRINK_EXPAND_MASK) {
case FAB_AND_BUTTONS_SHRINK:
@ -443,6 +438,7 @@ public class DeskClock extends BaseActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Recreate the activity if any settings have been changed
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SettingsMenuItemController.REQUEST_CHANGE_SETTINGS
&& resultCode == RESULT_OK) {
mRecreateActivity = true;
@ -458,8 +454,9 @@ public class DeskClock extends BaseActivity
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
if (requestCode == CODE_FOR_ALARM_PERMISSION){
@NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == CODE_FOR_ALARM_PERMISSION) {
LogUtils.i("Power off alarm permission is granted.");
}
}
@ -468,12 +465,13 @@ public class DeskClock extends BaseActivity
* Configure the {@link #mFragmentTabPager} and {@link #mBottomNavigation} to display
* UiDataModel's selected tab.
*/
@SuppressLint("ResourceType")
private void updateCurrentTab() {
// Fetch the selected tab from the source of truth: UiDataModel.
final UiDataModel.Tab selectedTab = UiDataModel.getUiDataModel().getSelectedTab();
// Update the selected tab in the mBottomNavigation if it does not agree with UiDataModel.
mBottomNavigation.setSelectedItemId(selectedTab.getPageResId());
// Update the selected fragment in the viewpager if it does not agree with UiDataModel.
for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
final DeskClockFragment fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i);
@ -482,8 +480,8 @@ public class DeskClock extends BaseActivity
break;
}
}
mTitleView.setText(selectedTab.getLabelResId());
mTitleView.setText(selectedTab.getLabelResId());
}
/**
@ -507,12 +505,19 @@ public class DeskClock extends BaseActivity
return Snackbar.make(mSnackbarAnchor, messageId, 5000 /* duration */);
}
/**
* Models the interesting state of display the {@link #mFab} button may inhabit.
*/
private enum FabState {SHOWING, HIDE_ARMED, HIDING}
/**
* As the view pager changes the selected page, update the model to record the new selected tab.
*/
private final class PageChangeWatcher implements OnPageChangeListener {
/** The last reported page scroll state; used to detect exotic state changes. */
/**
* The last reported page scroll state; used to detect exotic state changes.
*/
private int mPriorState = SCROLL_STATE_IDLE;
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
@ -640,13 +645,14 @@ public class DeskClock extends BaseActivity
}
}
/**
* As the model reports changes to the selected tab, update the user interface.
*/
private final class TabChangeWatcher implements TabListener {
@Override
public void selectedTabChanged(UiDataModel.Tab oldSelectedTab,
UiDataModel.Tab newSelectedTab) {
UiDataModel.Tab newSelectedTab) {
// Update the view pager and tab layout to agree with the model.
updateCurrentTab();

View file

@ -30,19 +30,6 @@ import com.best.deskclock.uidata.UiDataModel;
public class DeskClockApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
final Context applicationContext = getApplicationContext();
final SharedPreferences prefs = getDefaultSharedPreferences(applicationContext);
DataModel.getDataModel().init(applicationContext, prefs);
UiDataModel.getUiDataModel().init(applicationContext, prefs);
Controller.getController().setContext(applicationContext);
Controller.getController().addEventTracker(new LogEventTracker(applicationContext));
}
/**
* Returns the default {@link SharedPreferences} instance from the underlying storage context.
*/
@ -62,4 +49,17 @@ public class DeskClockApplication extends Application {
}
return PreferenceManager.getDefaultSharedPreferences(storageContext);
}
@Override
public void onCreate() {
super.onCreate();
final Context applicationContext = getApplicationContext();
final SharedPreferences prefs = getDefaultSharedPreferences(applicationContext);
DataModel.getDataModel().init(applicationContext, prefs);
UiDataModel.getUiDataModel().init(applicationContext, prefs);
Controller.getController().setContext(applicationContext);
Controller.getController().addEventTracker(new LogEventTracker(applicationContext));
}
}

View file

@ -26,6 +26,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import com.best.deskclock.alarms.AlarmStateManager;
@ -40,67 +41,9 @@ import java.util.List;
public class DeskClockBackupAgent extends BackupAgent {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DeskClockBackupAgent");
public static final String ACTION_COMPLETE_RESTORE =
"com.best.deskclock.action.COMPLETE_RESTORE";
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException { }
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException { }
@Override
public void onRestoreFile(@NonNull ParcelFileDescriptor data, long size, File destination,
int type, long mode, long mtime) throws IOException {
// The preference file on the backup device may not be the same on the restore device.
// Massage the file name here before writing it.
if (destination.getName().endsWith("_preferences.xml")) {
final String prefFileName = getPackageName() + "_preferences.xml";
destination = new File(destination.getParentFile(), prefFileName);
}
super.onRestoreFile(data, size, destination, type, mode, mtime);
}
/**
* When this method is called during backup/restore, the application is executing in a
* "minimalist" state. Because of this, the application's ContentResolver cannot be used.
* Consequently, the work of scheduling alarms on the restore device cannot be done here.
* Instead, a future callback to DeskClock is used as a signal to reschedule the alarms. The
* future callback may take the form of ACTION_BOOT_COMPLETED if the device is not yet fully
* booted (i.e. the restore occurred as part of the setup wizard). If the device is booted, an
* ACTION_COMPLETE_RESTORE broadcast is scheduled 10 seconds in the future to give
* backup/restore enough time to kill the Clock process. Both of these future callbacks result
* in the execution of {@link #processRestoredData(Context)}.
*/
@Override
public void onRestoreFinished() {
if (Utils.isNOrLater()) {
// TODO: migrate restored database and preferences over into
// the device-encrypted storage area
}
// Indicate a data restore has been completed.
DataModel.getDataModel().setRestoreBackupFinished(true);
// Create an Intent to send into DeskClock indicating restore is complete.
final PendingIntent restoreIntent = PendingIntent.getBroadcast(this, 0,
new Intent(ACTION_COMPLETE_RESTORE).setClass(this, AlarmInitReceiver.class),
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT);
// Deliver the Intent 10 seconds from now.
final long triggerAtMillis = SystemClock.elapsedRealtime() + 10000;
// Schedule the Intent delivery in AlarmManager.
final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, restoreIntent);
LOGGER.i("Waiting for %s to complete the data restore", ACTION_COMPLETE_RESTORE);
}
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DeskClockBackupAgent");
/**
* @param context a context to access resources and services
@ -129,7 +72,7 @@ public class DeskClockBackupAgent extends BackupAgent {
AlarmInstance alarmInstance = alarm.createInstanceAfter(now);
// Add the next alarm instance to the database.
alarmInstance = AlarmInstance.addInstance(contentResolver, alarmInstance);
AlarmInstance.addInstance(contentResolver, alarmInstance);
// Schedule the next alarm instance in AlarmManager.
AlarmStateManager.registerInstance(context, alarmInstance, true);
@ -143,4 +86,61 @@ public class DeskClockBackupAgent extends BackupAgent {
LOGGER.i("processRestoredData() completed");
return true;
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
}
@Override
public void onRestoreFile(@NonNull ParcelFileDescriptor data, long size, File destination,
int type, long mode, long mtime) throws IOException {
// The preference file on the backup device may not be the same on the restore device.
// Massage the file name here before writing it.
if (destination.getName().endsWith("_preferences.xml")) {
final String prefFileName = getPackageName() + "_preferences.xml";
destination = new File(destination.getParentFile(), prefFileName);
}
super.onRestoreFile(data, size, destination, type, mode, mtime);
}
/**
* When this method is called during backup/restore, the application is executing in a
* "minimalist" state. Because of this, the application's ContentResolver cannot be used.
* Consequently, the work of scheduling alarms on the restore device cannot be done here.
* Instead, a future callback to DeskClock is used as a signal to reschedule the alarms. The
* future callback may take the form of ACTION_BOOT_COMPLETED if the device is not yet fully
* booted (i.e. the restore occurred as part of the setup wizard). If the device is booted, an
* ACTION_COMPLETE_RESTORE broadcast is scheduled 10 seconds in the future to give
* backup/restore enough time to kill the Clock process. Both of these future callbacks result
* in the execution of {@link #processRestoredData(Context)}.
*/
@Override
public void onRestoreFinished() {
// TODO: migrate restored database and preferences over into
// the device-encrypted storage area
// Indicate a data restore has been completed.
DataModel.getDataModel().setRestoreBackupFinished(true);
// Create an Intent to send into DeskClock indicating restore is complete.
final PendingIntent restoreIntent = PendingIntent.getBroadcast(this, 0,
new Intent(ACTION_COMPLETE_RESTORE).setClass(this, AlarmInitReceiver.class),
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT);
// Deliver the Intent 10 seconds from now.
final long triggerAtMillis = SystemClock.elapsedRealtime() + 10000;
// Schedule the Intent delivery in AlarmManager.
final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, restoreIntent);
LOGGER.i("Waiting for %s to complete the data restore", ACTION_COMPLETE_RESTORE);
}
}

View file

@ -17,22 +17,26 @@
package com.best.deskclock;
import android.app.Fragment;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import android.view.KeyEvent;
import android.widget.Button;
import android.widget.ImageView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import com.best.deskclock.uidata.UiDataModel;
import com.best.deskclock.uidata.UiDataModel.Tab;
public abstract class DeskClockFragment extends Fragment implements FabContainer, FabController {
/** The tab associated with this fragment. */
/**
* The tab associated with this fragment.
*/
private final Tab mTab;
/** The container that houses the fab and its left and right buttons. */
/**
* The container that houses the fab and its left and right buttons.
*/
private FabContainer mFabContainer;
public DeskClockFragment(Tab tab) {

View file

@ -16,19 +16,21 @@
package com.best.deskclock;
import static com.best.deskclock.AnimatorUtils.getAlphaAnimator;
import android.animation.ValueAnimator;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.best.deskclock.data.DataModel;
import com.best.deskclock.uidata.TabScrollListener;
import com.best.deskclock.uidata.UiDataModel;
import com.best.deskclock.uidata.UiDataModel.Tab;
import static com.best.deskclock.AnimatorUtils.getAlphaAnimator;
/**
* This controller encapsulates the logic that watches a model for changes to scroll state and
* updates the display state of an associated drop shadow. The observable model may take many forms
@ -39,16 +41,24 @@ import static com.best.deskclock.AnimatorUtils.getAlphaAnimator;
*/
public final class DropShadowController {
/** Updates {@link #mDropShadowView} in response to changes in the backing scroll model. */
/**
* Updates {@link #mDropShadowView} in response to changes in the backing scroll model.
*/
private final ScrollChangeWatcher mScrollChangeWatcher = new ScrollChangeWatcher();
/** Fades the {@link @mDropShadowView} in/out as scroll state changes. */
/**
* Fades the {@link @mDropShadowView} in/out as scroll state changes.
*/
private final ValueAnimator mDropShadowAnimator;
/** The component that displays a drop shadow. */
/**
* The component that displays a drop shadow.
*/
private final View mDropShadowView;
/** Tab bar's hairline, which is hidden whenever the drop shadow is displayed. */
/**
* Tab bar's hairline, which is hidden whenever the drop shadow is displayed.
*/
private View mHairlineView;
// Supported sources of scroll position include: ListView, RecyclerView and UiDataModel.
@ -58,9 +68,9 @@ public final class DropShadowController {
/**
* @param dropShadowView to be hidden/shown as {@code uiDataModel} reports scrolling changes
* @param uiDataModel models the vertical scrolling state of the application's selected tab
* @param hairlineView at the bottom of the tab bar to be hidden or shown when the drop shadow
* is displayed or hidden, respectively.
* @param uiDataModel models the vertical scrolling state of the application's selected tab
* @param hairlineView at the bottom of the tab bar to be hidden or shown when the drop shadow
* is displayed or hidden, respectively.
*/
public DropShadowController(View dropShadowView, UiDataModel uiDataModel, View hairlineView) {
this(dropShadowView);
@ -72,7 +82,7 @@ public final class DropShadowController {
/**
* @param dropShadowView to be hidden/shown as {@code listView} reports scrolling changes
* @param listView a scrollable view that dictates the visibility of {@code dropShadowView}
* @param listView a scrollable view that dictates the visibility of {@code dropShadowView}
*/
public DropShadowController(View dropShadowView, ListView listView) {
this(dropShadowView);
@ -83,7 +93,7 @@ public final class DropShadowController {
/**
* @param dropShadowView to be hidden/shown as {@code recyclerView} reports scrolling changes
* @param recyclerView a scrollable view that dictates the visibility of {@code dropShadowView}
* @param recyclerView a scrollable view that dictates the visibility of {@code dropShadowView}
*/
public DropShadowController(View dropShadowView, RecyclerView recyclerView) {
this(dropShadowView);
@ -114,7 +124,7 @@ public final class DropShadowController {
/**
* @param shouldShowDropShadow {@code true} indicates the drop shadow should be displayed;
* {@code false} indicates the drop shadow should be hidden
* {@code false} indicates the drop shadow should be hidden
*/
private void updateDropShadow(boolean shouldShowDropShadow) {
if (!shouldShowDropShadow && mDropShadowView.getAlpha() != 0f) {
@ -148,17 +158,18 @@ public final class DropShadowController {
// RecyclerView scrolled.
@Override
public void onScrolled(RecyclerView view, int dx, int dy) {
public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
updateDropShadow(!Utils.isScrolledToTop(view));
}
// ListView scrolled.
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
int totalItemCount) {
updateDropShadow(!Utils.isScrolledToTop(view));
}

View file

@ -11,52 +11,73 @@ public interface FabContainer {
/** Bit field for updates */
/** Bit 0-1 */
/**
* Bit 0-1
*/
int FAB_ANIMATION_MASK = 0b11;
/** Signals that the fab should be updated in place with no animation. */
/**
* Signals that the fab should be updated in place with no animation.
*/
int FAB_IMMEDIATE = 0b1;
/** Signals the fab should be "animated away", updated, and "animated back". */
/**
* Signals the fab should be "animated away", updated, and "animated back".
*/
int FAB_SHRINK_AND_EXPAND = 0b10;
/** Signals that the fab should morph into a new state in place. */
/**
* Signals that the fab should morph into a new state in place.
*/
int FAB_MORPH = 0b11;
/** Bit 2 */
/**
* Bit 2
*/
int FAB_REQUEST_FOCUS_MASK = 0b100;
/** Signals that the fab should request focus. */
/**
* Signals that the fab should request focus.
*/
int FAB_REQUEST_FOCUS = 0b100;
/** Bit 3-4 */
/**
* Bit 3-4
*/
int BUTTONS_ANIMATION_MASK = 0b11000;
/** Signals that the buttons should be updated in place with no animation. */
/**
* Signals that the buttons should be updated in place with no animation.
*/
int BUTTONS_IMMEDIATE = 0b1000;
/** Signals that the buttons should be "animated away", updated, and "animated back". */
/**
* Signals that the buttons should be "animated away", updated, and "animated back".
*/
int BUTTONS_SHRINK_AND_EXPAND = 0b10000;
/** Bit 5 */
/**
* Bit 5
*/
int BUTTONS_DISABLE_MASK = 0b100000;
/** Disable the buttons of the fab so they do not respond to clicks. */
/**
* Disable the buttons of the fab so they do not respond to clicks.
*/
int BUTTONS_DISABLE = 0b100000;
/** Bit 6-7 */
/**
* Bit 6-7
*/
int FAB_AND_BUTTONS_SHRINK_EXPAND_MASK = 0b11000000;
/** Signals the fab and buttons should be "animated away". */
/**
* Signals the fab and buttons should be "animated away".
*/
int FAB_AND_BUTTONS_SHRINK = 0b10000000;
/** Signals the fab and buttons should be "animated back". */
/**
* Signals the fab and buttons should be "animated back".
*/
int FAB_AND_BUTTONS_EXPAND = 0b01000000;
/** Convenience flags */
/**
* Convenience flags
*/
int FAB_AND_BUTTONS_IMMEDIATE = FAB_IMMEDIATE | BUTTONS_IMMEDIATE;
int FAB_AND_BUTTONS_SHRINK_AND_EXPAND = FAB_SHRINK_AND_EXPAND | BUTTONS_SHRINK_AND_EXPAND;
@IntDef(
flag = true,
value = { FAB_IMMEDIATE, FAB_SHRINK_AND_EXPAND, FAB_MORPH, FAB_REQUEST_FOCUS,
BUTTONS_IMMEDIATE, BUTTONS_SHRINK_AND_EXPAND, BUTTONS_DISABLE,
FAB_AND_BUTTONS_IMMEDIATE, FAB_AND_BUTTONS_SHRINK_AND_EXPAND,
FAB_AND_BUTTONS_SHRINK, FAB_AND_BUTTONS_EXPAND }
)
@interface UpdateFabFlag {}
/**
* Requests that this container update the fab and/or its buttons because their state has
* changed. The update may be immediate or it may be animated depending on the choice of
@ -65,4 +86,14 @@ public interface FabContainer {
* @param updateTypes indicates the types of update to apply to the fab and its buttons
*/
void updateFab(@UpdateFabFlag int updateTypes);
@IntDef(
flag = true,
value = {FAB_IMMEDIATE, FAB_SHRINK_AND_EXPAND, FAB_MORPH, FAB_REQUEST_FOCUS,
BUTTONS_IMMEDIATE, BUTTONS_SHRINK_AND_EXPAND, BUTTONS_DISABLE,
FAB_AND_BUTTONS_IMMEDIATE, FAB_AND_BUTTONS_SHRINK_AND_EXPAND,
FAB_AND_BUTTONS_SHRINK, FAB_AND_BUTTONS_EXPAND}
)
@interface UpdateFabFlag {
}
}

View file

@ -1,10 +1,11 @@
package com.best.deskclock;
import androidx.annotation.NonNull;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import androidx.annotation.NonNull;
/**
* Implementers of this interface are able to {@link #onUpdateFab configure the fab} and associated
* {@link #onUpdateFabButtons left/right buttons} including setting them {@link View#INVISIBLE} if
@ -32,7 +33,7 @@ public interface FabController {
* Configures the display of the buttons to the left and right of the fab to match the current
* state of this controller.
*
* @param left button to the left of the fab to configure based on current state
* @param left button to the left of the fab to configure based on current state
* @param right button to the right of the fab to configure based on current state
*/
void onUpdateFabButtons(@NonNull Button left, @NonNull Button right);

View file

@ -20,7 +20,6 @@ import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import android.provider.AlarmClock;
import com.best.deskclock.alarms.AlarmStateManager;
@ -163,7 +162,7 @@ class FetchMatchingAlarmsAction implements Runnable {
// if we want to dismiss we should only add enabled alarms
final String selection = String.format("%s=? AND %s=? AND %s=?",
Alarm.HOUR, Alarm.MINUTES, Alarm.ENABLED);
final String[] args = { String.valueOf(hour24), String.valueOf(minutes), "1" };
final String[] args = {String.valueOf(hour24), String.valueOf(minutes), "1"};
return Alarm.getAlarms(cr, selection, args);
}

View file

@ -19,12 +19,14 @@ package com.best.deskclock;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import androidx.legacy.app.FragmentCompat;
import androidx.viewpager.widget.PagerAdapter;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.legacy.app.FragmentCompat;
import androidx.viewpager.widget.PagerAdapter;
import com.best.deskclock.uidata.UiDataModel;
import java.util.Map;
@ -40,16 +42,24 @@ final class FragmentTabPagerAdapter extends PagerAdapter {
private final DeskClock mDeskClock;
/** The manager into which fragments are added. */
/**
* The manager into which fragments are added.
*/
private final FragmentManager mFragmentManager;
/** A fragment cache that can be accessed before {@link #instantiateItem} is called. */
/**
* A fragment cache that can be accessed before {@link #instantiateItem} is called.
*/
private final Map<UiDataModel.Tab, DeskClockFragment> mFragmentCache;
/** The active fragment transaction if one exists. */
/**
* The active fragment transaction if one exists.
*/
private FragmentTransaction mCurrentTransaction;
/** The current fragment displayed to the user. */
/**
* The current fragment displayed to the user.
*/
private Fragment mCurrentPrimaryItem;
FragmentTabPagerAdapter(DeskClock deskClock) {
@ -102,8 +112,9 @@ final class FragmentTabPagerAdapter extends PagerAdapter {
}
}
@NonNull
@Override
public Object instantiateItem(ViewGroup container, int position) {
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
@ -127,7 +138,7 @@ final class FragmentTabPagerAdapter extends PagerAdapter {
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
if (mCurrentTransaction == null) {
mCurrentTransaction = mFragmentManager.beginTransaction();
}
@ -137,7 +148,7 @@ final class FragmentTabPagerAdapter extends PagerAdapter {
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
final Fragment fragment = (Fragment) object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
@ -153,7 +164,7 @@ final class FragmentTabPagerAdapter extends PagerAdapter {
}
@Override
public void finishUpdate(ViewGroup container) {
public void finishUpdate(@NonNull ViewGroup container) {
if (mCurrentTransaction != null) {
mCurrentTransaction.commitAllowingStateLoss();
mCurrentTransaction = null;
@ -162,7 +173,7 @@ final class FragmentTabPagerAdapter extends PagerAdapter {
}
@Override
public boolean isViewFromObject(View view, Object object) {
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return ((Fragment) object).getView() == view;
}
}

View file

@ -16,6 +16,15 @@
package com.best.deskclock;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static com.best.deskclock.AlarmSelectionActivity.ACTION_DISMISS;
import static com.best.deskclock.AlarmSelectionActivity.EXTRA_ACTION;
import static com.best.deskclock.AlarmSelectionActivity.EXTRA_ALARMS;
import static com.best.deskclock.provider.AlarmInstance.FIRED_STATE;
import static com.best.deskclock.provider.AlarmInstance.SNOOZE_STATE;
import static com.best.deskclock.uidata.UiDataModel.Tab.ALARMS;
import static com.best.deskclock.uidata.UiDataModel.Tab.TIMERS;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
@ -47,15 +56,6 @@ import java.util.Date;
import java.util.Iterator;
import java.util.List;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import static com.best.deskclock.AlarmSelectionActivity.ACTION_DISMISS;
import static com.best.deskclock.AlarmSelectionActivity.EXTRA_ACTION;
import static com.best.deskclock.AlarmSelectionActivity.EXTRA_ALARMS;
import static com.best.deskclock.provider.AlarmInstance.FIRED_STATE;
import static com.best.deskclock.provider.AlarmInstance.SNOOZE_STATE;
import static com.best.deskclock.uidata.UiDataModel.Tab.ALARMS;
import static com.best.deskclock.uidata.UiDataModel.Tab.TIMERS;
/**
* This activity is never visible. It processes all public intents defined by {@link AlarmClock}
* that apply to alarms and timers. Its definition in AndroidManifest.xml requires callers to hold
@ -67,6 +67,113 @@ public class HandleApiCalls extends Activity {
private Context mAppContext;
public static void dismissAlarm(Alarm alarm, Activity activity) {
final Context context = activity.getApplicationContext();
final AlarmInstance instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
context.getContentResolver(), alarm.id);
if (instance == null) {
final String reason = context.getString(R.string.no_alarm_scheduled_for_this_time);
Controller.getController().notifyVoiceFailure(activity, reason);
LOGGER.i("No alarm instance to dismiss");
return;
}
dismissAlarmInstance(instance, activity);
}
public static void dismissAlarmInstance(AlarmInstance instance, Activity activity) {
Utils.enforceNotMainLooper();
final Context context = activity.getApplicationContext();
final Date alarmTime = instance.getAlarmTime().getTime();
final String time = DateFormat.getTimeFormat(context).format(alarmTime);
if (instance.mAlarmState == FIRED_STATE || instance.mAlarmState == SNOOZE_STATE) {
// Always dismiss alarms that are fired or snoozed.
AlarmStateManager.deleteInstanceAndUpdateParent(context, instance);
} else if (Utils.isAlarmWithin24Hours(instance)) {
// Upcoming alarms are always predismissed.
AlarmStateManager.setPreDismissState(context, instance);
} else {
// Otherwise the alarm cannot be dismissed at this time.
final String reason = context.getString(
R.string.alarm_cant_be_dismissed_still_more_than_24_hours_away, time);
Controller.getController().notifyVoiceFailure(activity, reason);
LOGGER.i("Can't dismiss alarm more than 24 hours in advance");
}
// Log the successful dismissal.
final String reason = context.getString(R.string.alarm_is_dismissed, time);
Controller.getController().notifyVoiceSuccess(activity, reason);
LOGGER.i("Alarm dismissed: " + instance);
Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
}
static void snoozeAlarm(AlarmInstance alarmInstance, Context context, Activity activity) {
Utils.enforceNotMainLooper();
final String time = DateFormat.getTimeFormat(context).format(
alarmInstance.getAlarmTime().getTime());
final String reason = context.getString(R.string.alarm_is_snoozed, time);
AlarmStateManager.setSnoozeState(context, alarmInstance, true);
Controller.getController().notifyVoiceSuccess(activity, reason);
LOGGER.i("Alarm snoozed: " + alarmInstance);
Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
}
/**
* @param alarm the alarm to be updated
* @param intent the intent containing new alarm field values to merge into the {@code alarm}
*/
private static void updateAlarmFromIntent(Alarm alarm, Intent intent) {
alarm.enabled = true;
alarm.hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, alarm.hour);
alarm.minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, alarm.minutes);
alarm.vibrate = intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, alarm.vibrate);
alarm.alert = getAlertFromIntent(intent, alarm.alert);
alarm.label = getLabelFromIntent(intent, alarm.label);
alarm.daysOfWeek = getDaysFromIntent(intent, alarm.daysOfWeek);
}
private static String getLabelFromIntent(Intent intent, String defaultLabel) {
final String message = intent.getExtras().getString(AlarmClock.EXTRA_MESSAGE, defaultLabel);
return message == null ? "" : message;
}
private static Weekdays getDaysFromIntent(Intent intent, Weekdays defaultWeekdays) {
if (!intent.hasExtra(AlarmClock.EXTRA_DAYS)) {
return defaultWeekdays;
}
final List<Integer> days = intent.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS);
if (days != null) {
final int[] daysArray = new int[days.size()];
for (int i = 0; i < days.size(); i++) {
daysArray[i] = days.get(i);
}
return Weekdays.fromCalendarDays(daysArray);
} else {
// API says to use an ArrayList<Integer> but we allow the user to use a int[] too.
final int[] daysArray = intent.getIntArrayExtra(AlarmClock.EXTRA_DAYS);
if (daysArray != null) {
return Weekdays.fromCalendarDays(daysArray);
}
}
return defaultWeekdays;
}
private static Uri getAlertFromIntent(Intent intent, Uri defaultUri) {
final String alert = intent.getStringExtra(AlarmClock.EXTRA_RINGTONE);
if (alert == null) {
return defaultUri;
} else if (AlarmClock.VALUE_RINGTONE_SILENT.equals(alert) || alert.isEmpty()) {
return Alarm.NO_RINGTONE_URI;
}
return Uri.parse(alert);
}
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
@ -112,7 +219,6 @@ public class HandleApiCalls extends Activity {
}
}
private void handleDismissAlarm(Intent intent) {
// Change to the alarms tab.
UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
@ -123,177 +229,10 @@ public class HandleApiCalls extends Activity {
new DismissAlarmAsync(mAppContext, intent, this).execute();
}
public static void dismissAlarm(Alarm alarm, Activity activity) {
final Context context = activity.getApplicationContext();
final AlarmInstance instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
context.getContentResolver(), alarm.id);
if (instance == null) {
final String reason = context.getString(R.string.no_alarm_scheduled_for_this_time);
Controller.getController().notifyVoiceFailure(activity, reason);
LOGGER.i("No alarm instance to dismiss");
return;
}
dismissAlarmInstance(instance, activity);
}
public static void dismissAlarmInstance(AlarmInstance instance, Activity activity) {
Utils.enforceNotMainLooper();
final Context context = activity.getApplicationContext();
final Date alarmTime = instance.getAlarmTime().getTime();
final String time = DateFormat.getTimeFormat(context).format(alarmTime);
if (instance.mAlarmState == FIRED_STATE || instance.mAlarmState == SNOOZE_STATE) {
// Always dismiss alarms that are fired or snoozed.
AlarmStateManager.deleteInstanceAndUpdateParent(context, instance);
} else if (Utils.isAlarmWithin24Hours(instance)) {
// Upcoming alarms are always predismissed.
AlarmStateManager.setPreDismissState(context, instance);
} else {
// Otherwise the alarm cannot be dismissed at this time.
final String reason = context.getString(
R.string.alarm_cant_be_dismissed_still_more_than_24_hours_away, time);
Controller.getController().notifyVoiceFailure(activity, reason);
LOGGER.i("Can't dismiss alarm more than 24 hours in advance");
}
// Log the successful dismissal.
final String reason = context.getString(R.string.alarm_is_dismissed, time);
Controller.getController().notifyVoiceSuccess(activity, reason);
LOGGER.i("Alarm dismissed: " + instance);
Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
}
private static class DismissAlarmAsync extends AsyncTask<Void, Void, Void> {
private final Context mContext;
private final Intent mIntent;
private final Activity mActivity;
public DismissAlarmAsync(Context context, Intent intent, Activity activity) {
mContext = context;
mIntent = intent;
mActivity = activity;
}
@Override
protected Void doInBackground(Void... parameters) {
final ContentResolver cr = mContext.getContentResolver();
final List<Alarm> alarms = getEnabledAlarms(mContext);
if (alarms.isEmpty()) {
final String reason = mContext.getString(R.string.no_scheduled_alarms);
Controller.getController().notifyVoiceFailure(mActivity, reason);
LOGGER.i("No scheduled alarms");
return null;
}
// remove Alarms in MISSED, DISMISSED, and PREDISMISSED states
for (Iterator<Alarm> i = alarms.iterator(); i.hasNext();) {
final AlarmInstance instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
cr, i.next().id);
if (instance == null || instance.mAlarmState > FIRED_STATE) {
i.remove();
}
}
final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE);
if (searchMode == null && alarms.size() > 1) {
// shows the UI where user picks which alarm they want to DISMISS
final Intent pickSelectionIntent = new Intent(mContext,
AlarmSelectionActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_ACTION, ACTION_DISMISS)
.putExtra(EXTRA_ALARMS, alarms.toArray(new Parcelable[alarms.size()]));
mContext.startActivity(pickSelectionIntent);
final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
return null;
}
// fetch the alarms that are specified by the intent
final FetchMatchingAlarmsAction fmaa =
new FetchMatchingAlarmsAction(mContext, alarms, mIntent, mActivity);
fmaa.run();
final List<Alarm> matchingAlarms = fmaa.getMatchingAlarms();
// If there are multiple matching alarms and it wasn't expected
// disambiguate what the user meant
if (!AlarmClock.ALARM_SEARCH_MODE_ALL.equals(searchMode) && matchingAlarms.size() > 1) {
final Intent pickSelectionIntent = new Intent(mContext, AlarmSelectionActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_ACTION, ACTION_DISMISS)
.putExtra(EXTRA_ALARMS,
matchingAlarms.toArray(new Parcelable[matchingAlarms.size()]));
mContext.startActivity(pickSelectionIntent);
final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
return null;
}
// Apply the action to the matching alarms
for (Alarm alarm : matchingAlarms) {
dismissAlarm(alarm, mActivity);
LOGGER.i("Alarm dismissed: " + alarm);
}
return null;
}
private static List<Alarm> getEnabledAlarms(Context context) {
final String selection = String.format("%s=?", Alarm.ENABLED);
final String[] args = { "1" };
return Alarm.getAlarms(context.getContentResolver(), selection, args);
}
}
private void handleSnoozeAlarm(Intent intent) {
new SnoozeAlarmAsync(intent, this).execute();
}
private static class SnoozeAlarmAsync extends AsyncTask<Void, Void, Void> {
private final Context mContext;
private final Intent mIntent;
private final Activity mActivity;
public SnoozeAlarmAsync(Intent intent, Activity activity) {
mContext = activity.getApplicationContext();
mIntent = intent;
mActivity = activity;
}
@Override
protected Void doInBackground(Void... parameters) {
final ContentResolver cr = mContext.getContentResolver();
final List<AlarmInstance> alarmInstances = AlarmInstance.getInstancesByState(
cr, FIRED_STATE);
if (alarmInstances.isEmpty()) {
final String reason = mContext.getString(R.string.no_firing_alarms);
Controller.getController().notifyVoiceFailure(mActivity, reason);
LOGGER.i("No firing alarms");
return null;
}
for (AlarmInstance firingAlarmInstance : alarmInstances) {
snoozeAlarm(firingAlarmInstance, mContext, mActivity);
}
return null;
}
}
static void snoozeAlarm(AlarmInstance alarmInstance, Context context, Activity activity) {
Utils.enforceNotMainLooper();
final String time = DateFormat.getTimeFormat(context).format(
alarmInstance.getAlarmTime().getTime());
final String reason = context.getString(R.string.alarm_is_snoozed, time);
AlarmStateManager.setSnoozeState(context, alarmInstance, true);
Controller.getController().notifyVoiceSuccess(activity, reason);
LOGGER.i("Alarm snoozed: " + alarmInstance);
Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
}
/***
* Processes the SET_ALARM intent
* @param intent Intent passed to the app
@ -479,9 +418,15 @@ public class HandleApiCalls extends Activity {
// Attempt to reuse an existing timer that is Reset with the same length and label.
Timer timer = null;
for (Timer t : DataModel.getDataModel().getTimers()) {
if (!t.isReset()) { continue; }
if (t.getLength() != lengthMillis) { continue; }
if (!TextUtils.equals(label, t.getLabel())) { continue; }
if (!t.isReset()) {
continue;
}
if (t.getLength() != lengthMillis) {
continue;
}
if (!TextUtils.equals(label, t.getLabel())) {
continue;
}
timer = t;
break;
@ -510,7 +455,7 @@ public class HandleApiCalls extends Activity {
}
private void setupInstance(AlarmInstance instance, boolean skipUi) {
instance = AlarmInstance.addInstance(this.getContentResolver(), instance);
AlarmInstance.addInstance(this.getContentResolver(), instance);
AlarmStateManager.registerInstance(this, instance, true);
AlarmUtils.popAlarmSetToast(this, instance.getAlarmTime().getTimeInMillis());
if (!skipUi) {
@ -525,58 +470,6 @@ public class HandleApiCalls extends Activity {
}
}
/**
* @param alarm the alarm to be updated
* @param intent the intent containing new alarm field values to merge into the {@code alarm}
*/
private static void updateAlarmFromIntent(Alarm alarm, Intent intent) {
alarm.enabled = true;
alarm.hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, alarm.hour);
alarm.minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, alarm.minutes);
alarm.vibrate = intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, alarm.vibrate);
alarm.alert = getAlertFromIntent(intent, alarm.alert);
alarm.label = getLabelFromIntent(intent, alarm.label);
alarm.daysOfWeek = getDaysFromIntent(intent, alarm.daysOfWeek);
}
private static String getLabelFromIntent(Intent intent, String defaultLabel) {
final String message = intent.getExtras().getString(AlarmClock.EXTRA_MESSAGE, defaultLabel);
return message == null ? "" : message;
}
private static Weekdays getDaysFromIntent(Intent intent, Weekdays defaultWeekdays) {
if (!intent.hasExtra(AlarmClock.EXTRA_DAYS)) {
return defaultWeekdays;
}
final List<Integer> days = intent.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS);
if (days != null) {
final int[] daysArray = new int[days.size()];
for (int i = 0; i < days.size(); i++) {
daysArray[i] = days.get(i);
}
return Weekdays.fromCalendarDays(daysArray);
} else {
// API says to use an ArrayList<Integer> but we allow the user to use a int[] too.
final int[] daysArray = intent.getIntArrayExtra(AlarmClock.EXTRA_DAYS);
if (daysArray != null) {
return Weekdays.fromCalendarDays(daysArray);
}
}
return defaultWeekdays;
}
private static Uri getAlertFromIntent(Intent intent, Uri defaultUri) {
final String alert = intent.getStringExtra(AlarmClock.EXTRA_RINGTONE);
if (alert == null) {
return defaultUri;
} else if (AlarmClock.VALUE_RINGTONE_SILENT.equals(alert) || alert.isEmpty()) {
return Alarm.NO_RINGTONE_URI;
}
return Uri.parse(alert);
}
/**
* Assemble a database where clause to search for an alarm matching the given {@code hour} and
* {@code minutes} as well as all of the optional information within the {@code intent}
@ -589,11 +482,11 @@ public class HandleApiCalls extends Activity {
* <li>ringtone uri</li>
* </ul>
*
* @param intent contains details of the alarm to be located
* @param hour the hour of the day of the alarm
* @param minutes the minute of the hour of the alarm
* @param intent contains details of the alarm to be located
* @param hour the hour of the day of the alarm
* @param minutes the minute of the hour of the alarm
* @param selection an out parameter containing a SQL where clause
* @param args an out parameter containing the values to substitute into the {@code selection}
* @param args an out parameter containing the values to substitute into the {@code selection}
*/
private void setSelectionFromIntent(
Intent intent,
@ -630,4 +523,114 @@ public class HandleApiCalls extends Activity {
args.add(ringtone.toString());
}
}
private static class DismissAlarmAsync extends AsyncTask<Void, Void, Void> {
private final Context mContext;
private final Intent mIntent;
private final Activity mActivity;
public DismissAlarmAsync(Context context, Intent intent, Activity activity) {
mContext = context;
mIntent = intent;
mActivity = activity;
}
private static List<Alarm> getEnabledAlarms(Context context) {
final String selection = String.format("%s=?", Alarm.ENABLED);
final String[] args = {"1"};
return Alarm.getAlarms(context.getContentResolver(), selection, args);
}
@Override
protected Void doInBackground(Void... parameters) {
final ContentResolver cr = mContext.getContentResolver();
final List<Alarm> alarms = getEnabledAlarms(mContext);
if (alarms.isEmpty()) {
final String reason = mContext.getString(R.string.no_scheduled_alarms);
Controller.getController().notifyVoiceFailure(mActivity, reason);
LOGGER.i("No scheduled alarms");
return null;
}
// remove Alarms in MISSED, DISMISSED, and PREDISMISSED states
for (Iterator<Alarm> i = alarms.iterator(); i.hasNext(); ) {
final AlarmInstance instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
cr, i.next().id);
if (instance == null || instance.mAlarmState > FIRED_STATE) {
i.remove();
}
}
final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE);
if (searchMode == null && alarms.size() > 1) {
// shows the UI where user picks which alarm they want to DISMISS
final Intent pickSelectionIntent = new Intent(mContext,
AlarmSelectionActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_ACTION, ACTION_DISMISS)
.putExtra(EXTRA_ALARMS, alarms.toArray(new Parcelable[alarms.size()]));
mContext.startActivity(pickSelectionIntent);
final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
return null;
}
// fetch the alarms that are specified by the intent
final FetchMatchingAlarmsAction fmaa =
new FetchMatchingAlarmsAction(mContext, alarms, mIntent, mActivity);
fmaa.run();
final List<Alarm> matchingAlarms = fmaa.getMatchingAlarms();
// If there are multiple matching alarms and it wasn't expected
// disambiguate what the user meant
if (!AlarmClock.ALARM_SEARCH_MODE_ALL.equals(searchMode) && matchingAlarms.size() > 1) {
final Intent pickSelectionIntent = new Intent(mContext, AlarmSelectionActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_ACTION, ACTION_DISMISS)
.putExtra(EXTRA_ALARMS,
matchingAlarms.toArray(new Parcelable[matchingAlarms.size()]));
mContext.startActivity(pickSelectionIntent);
final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
return null;
}
// Apply the action to the matching alarms
for (Alarm alarm : matchingAlarms) {
dismissAlarm(alarm, mActivity);
LOGGER.i("Alarm dismissed: " + alarm);
}
return null;
}
}
private static class SnoozeAlarmAsync extends AsyncTask<Void, Void, Void> {
private final Context mContext;
private final Activity mActivity;
public SnoozeAlarmAsync(Intent intent, Activity activity) {
mContext = activity.getApplicationContext();
mActivity = activity;
}
@Override
protected Void doInBackground(Void... parameters) {
final ContentResolver cr = mContext.getContentResolver();
final List<AlarmInstance> alarmInstances = AlarmInstance.getInstancesByState(
cr, FIRED_STATE);
if (alarmInstances.isEmpty()) {
final String reason = mContext.getString(R.string.no_firing_alarms);
Controller.getController().notifyVoiceFailure(mActivity, reason);
LOGGER.i("No firing alarms");
return null;
}
for (AlarmInstance firingAlarmInstance : alarmInstances) {
snoozeAlarm(firingAlarmInstance, mContext, mActivity);
}
return null;
}
}
}

View file

@ -16,6 +16,8 @@
package com.best.deskclock;
import static com.best.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
@ -24,8 +26,6 @@ import com.best.deskclock.events.Events;
import com.best.deskclock.stopwatch.StopwatchService;
import com.best.deskclock.uidata.UiDataModel;
import static com.best.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
public class HandleShortcuts extends Activity {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("HandleShortcuts");

View file

@ -16,18 +16,19 @@
package com.best.deskclock;
import static androidx.recyclerview.widget.RecyclerView.NO_ID;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import static androidx.recyclerview.widget.RecyclerView.NO_ID;
/**
* Base adapter class for displaying a collection of items. Provides functionality for handling
* changing items, persistent item state, item click events, and re-usable item views.
@ -35,6 +36,36 @@ import static androidx.recyclerview.widget.RecyclerView.NO_ID;
public class ItemAdapter<T extends ItemAdapter.ItemHolder>
extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
/**
* Factories for creating new {@link ItemViewHolder} entities.
*/
private final SparseArray<ItemViewHolder.Factory> mFactoriesByViewType = new SparseArray<>();
/**
* Listeners to invoke in {@link #mOnItemClickedListener}.
*/
private final SparseArray<OnItemClickedListener> mListenersByViewType = new SparseArray<>();
/**
* Invokes the {@link OnItemClickedListener} in {@link #mListenersByViewType} corresponding
* to {@link ItemViewHolder#getItemViewType()}
*/
private final OnItemClickedListener mOnItemClickedListener = new OnItemClickedListener() {
@Override
public void onItemClicked(ItemViewHolder<?> viewHolder, int id) {
final OnItemClickedListener listener =
mListenersByViewType.get(viewHolder.getItemViewType());
if (listener != null) {
listener.onItemClicked(viewHolder, id);
}
}
};
/**
* Invoked when any item changes.
*/
private OnItemChangedListener mOnItemChangedListener;
/**
* List of current item holders represented by this adapter.
*/
private List<T> mItemHolders;
/**
* Finds the position of the changed item holder and invokes {@link #notifyItemChanged(int)} or
* {@link #notifyItemChanged(int, Object)} if payloads are present (in order to do in-place
@ -64,41 +95,6 @@ public class ItemAdapter<T extends ItemAdapter.ItemHolder>
}
};
/**
* Invokes the {@link OnItemClickedListener} in {@link #mListenersByViewType} corresponding
* to {@link ItemViewHolder#getItemViewType()}
*/
private final OnItemClickedListener mOnItemClickedListener = new OnItemClickedListener() {
@Override
public void onItemClicked(ItemViewHolder<?> viewHolder, int id) {
final OnItemClickedListener listener =
mListenersByViewType.get(viewHolder.getItemViewType());
if (listener != null) {
listener.onItemClicked(viewHolder, id);
}
}
};
/**
* Invoked when any item changes.
*/
private OnItemChangedListener mOnItemChangedListener;
/**
* Factories for creating new {@link ItemViewHolder} entities.
*/
private final SparseArray<ItemViewHolder.Factory> mFactoriesByViewType = new SparseArray<>();
/**
* Listeners to invoke in {@link #mOnItemClickedListener}.
*/
private final SparseArray<OnItemClickedListener> mListenersByViewType = new SparseArray<>();
/**
* List of current item holders represented by this adapter.
*/
private List<T> mItemHolders;
/**
* Convenience for calling {@link #setHasStableIds(boolean)} with {@code true}.
*
@ -120,7 +116,7 @@ public class ItemAdapter<T extends ItemAdapter.ItemHolder>
* @return this object, allowing calls to methods in this class to be chained
*/
public ItemAdapter withViewTypes(ItemViewHolder.Factory factory,
OnItemClickedListener listener, int... viewTypes) {
OnItemClickedListener listener, int... viewTypes) {
for (int viewType : viewTypes) {
mFactoriesByViewType.put(viewType, factory);
mListenersByViewType.put(viewType, listener);
@ -257,8 +253,9 @@ public class ItemAdapter<T extends ItemAdapter.ItemHolder>
return mItemHolders.get(position).getItemViewType();
}
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final ItemViewHolder.Factory factory = mFactoriesByViewType.get(viewType);
if (factory != null) {
return factory.createViewHolder(parent, viewType);
@ -281,6 +278,40 @@ public class ItemAdapter<T extends ItemAdapter.ItemHolder>
viewHolder.recycleItemView();
}
/**
* Callback interface for when an item changes and should be re-bound.
*/
public interface OnItemChangedListener {
/**
* Invoked by {@link ItemHolder#notifyItemChanged()}.
*
* @param itemHolder the item holder that has changed
*/
void onItemChanged(ItemHolder<?> itemHolder);
/**
* Invoked by {@link ItemHolder#notifyItemChanged(Object payload)}.
*
* @param itemHolder the item holder that has changed
* @param payload the payload object
*/
void onItemChanged(ItemAdapter.ItemHolder<?> itemHolder, Object payload);
}
/**
* Callback interface for handling when an item is clicked.
*/
public interface OnItemClickedListener {
/**
* Invoked by {@link ItemViewHolder#notifyItemClicked(int)}
*
* @param viewHolder the {@link ItemViewHolder} containing the view that was clicked
* @param id the unique identifier for the click action that has occurred
*/
void onItemClicked(ItemViewHolder<?> viewHolder, int id);
}
/**
* Base class for wrapping an item for compatibility with an {@link ItemHolder}.
* <p/>
@ -504,41 +535,7 @@ public class ItemAdapter<T extends ItemAdapter.ItemHolder>
* @param viewType the unique id of the item view to create
* @return a new initialized {@link ItemViewHolder}
*/
public ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType);
ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType);
}
}
/**
* Callback interface for when an item changes and should be re-bound.
*/
public interface OnItemChangedListener {
/**
* Invoked by {@link ItemHolder#notifyItemChanged()}.
*
* @param itemHolder the item holder that has changed
*/
void onItemChanged(ItemHolder<?> itemHolder);
/**
* Invoked by {@link ItemHolder#notifyItemChanged(Object payload)}.
*
* @param itemHolder the item holder that has changed
* @param payload the payload object
*/
void onItemChanged(ItemAdapter.ItemHolder<?> itemHolder, Object payload);
}
/**
* Callback interface for handling when an item is clicked.
*/
public interface OnItemClickedListener {
/**
* Invoked by {@link ItemViewHolder#notifyItemClicked(int)}
*
* @param viewHolder the {@link ItemViewHolder} containing the view that was clicked
* @param id the unique identifier for the click action that has occurred
*/
void onItemClicked(ItemViewHolder<?> viewHolder, int id);
}
}

View file

@ -16,25 +16,26 @@
package com.best.deskclock;
import static android.view.View.TRANSLATION_X;
import static android.view.View.TRANSLATION_Y;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.collection.ArrayMap;
import androidx.recyclerview.widget.RecyclerView.State;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import androidx.recyclerview.widget.SimpleItemAnimator;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static android.view.View.TRANSLATION_Y;
import static android.view.View.TRANSLATION_X;
public class ItemAnimator extends SimpleItemAnimator {
private final List<Animator> mAddAnimatorsList = new ArrayList<>();
@ -156,8 +157,8 @@ public class ItemAnimator extends SimpleItemAnimator {
@Override
public boolean animateChange(@NonNull final ViewHolder oldHolder,
@NonNull final ViewHolder newHolder, @NonNull ItemHolderInfo preInfo,
@NonNull ItemHolderInfo postInfo) {
@NonNull final ViewHolder newHolder, @NonNull ItemHolderInfo preInfo,
@NonNull ItemHolderInfo postInfo) {
endAnimation(oldHolder);
endAnimation(newHolder);
@ -246,7 +247,7 @@ public class ItemAnimator extends SimpleItemAnimator {
@Override
public boolean animateChange(ViewHolder oldHolder,
ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
/* Unused */
throw new IllegalStateException("This method should not be used");
}
@ -289,7 +290,7 @@ public class ItemAnimator extends SimpleItemAnimator {
}
@Override
public void endAnimation(ViewHolder holder) {
public void endAnimation(@NonNull ViewHolder holder) {
final Animator animator = mAnimators.get(holder);
mAnimators.remove(holder);
@ -326,9 +327,10 @@ public class ItemAnimator extends SimpleItemAnimator {
}
@Override
public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
@NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
@NonNull List<Object> payloads) {
public @NonNull
ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
@NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
@NonNull List<Object> payloads) {
final ItemHolderInfo itemHolderInfo = super.recordPreLayoutInformation(state, viewHolder,
changeFlags, payloads);
if (itemHolderInfo instanceof PayloadItemHolderInfo) {
@ -337,6 +339,7 @@ public class ItemAnimator extends SimpleItemAnimator {
return itemHolderInfo;
}
@NonNull
@Override
public ItemHolderInfo obtainHolderInfo() {
return new PayloadItemHolderInfo();
@ -344,28 +347,29 @@ public class ItemAnimator extends SimpleItemAnimator {
@Override
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
@NonNull List<Object> payloads) {
@NonNull List<Object> payloads) {
final boolean defaultReusePolicy = super.canReuseUpdatedViewHolder(viewHolder, payloads);
// Whenever we have a payload, this is an in-place animation.
return !payloads.isEmpty() || defaultReusePolicy;
}
public interface OnAnimateChangeListener {
Animator onAnimateChange(ViewHolder oldHolder, ViewHolder newHolder, long duration);
Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
int fromBottom, long duration);
}
private static final class PayloadItemHolderInfo extends ItemHolderInfo {
private final List<Object> mPayloads = new ArrayList<>();
List<Object> getPayloads() {
return mPayloads;
}
void setPayloads(List<Object> payloads) {
mPayloads.clear();
mPayloads.addAll(payloads);
}
List<Object> getPayloads() {
return mPayloads;
}
}
public interface OnAnimateChangeListener {
Animator onAnimateChange(ViewHolder oldHolder, ViewHolder newHolder, long duration);
Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
int fromBottom, long duration);
}
}

View file

@ -16,6 +16,8 @@
package com.best.deskclock;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
@ -24,11 +26,7 @@ import android.app.FragmentTransaction;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatEditText;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
@ -38,12 +36,15 @@ import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatEditText;
import com.best.deskclock.data.DataModel;
import com.best.deskclock.data.Timer;
import com.best.deskclock.provider.Alarm;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
import java.util.Objects;
/**
* DialogFragment to edit label.
@ -114,7 +115,7 @@ public class LabelDialogFragment extends DialogFragment {
super.onSaveInstanceState(outState);
// As long as the label box exists, save its state.
if (mLabelBox != null) {
outState.putString(ARG_LABEL, mLabelBox.getText().toString());
outState.putString(ARG_LABEL, Objects.requireNonNull(mLabelBox.getText()).toString());
}
}
@ -138,14 +139,14 @@ public class LabelDialogFragment extends DialogFragment {
final Context context = dialog.getContext();
final int colorControlActivated =
ThemeUtils.resolveColor(context, R.attr.colorControlActivated);
ThemeUtils.resolveColor(context, androidx.appcompat.R.attr.colorControlActivated);
final int colorControlNormal =
ThemeUtils.resolveColor(context, R.attr.colorControlNormal);
ThemeUtils.resolveColor(context, androidx.appcompat.R.attr.colorControlNormal);
mLabelBox = new AppCompatEditText(context);
mLabelBox.setSupportBackgroundTintList(new ColorStateList(
new int[][] { { android.R.attr.state_activated }, {} },
new int[] { colorControlActivated, colorControlNormal }));
new int[][]{{android.R.attr.state_activated}, {}},
new int[]{colorControlActivated, colorControlNormal}));
mLabelBox.setOnEditorActionListener(new ImeDoneListener());
mLabelBox.addTextChangedListener(new TextChangeListener());
mLabelBox.setSingleLine();
@ -178,7 +179,7 @@ public class LabelDialogFragment extends DialogFragment {
* Sets the new label into the timer or alarm.
*/
private void setLabel() {
String label = mLabelBox.getText().toString();
String label = Objects.requireNonNull(mLabelBox.getText()).toString();
if (label.trim().isEmpty()) {
// Don't allow user to input label with only whitespace.
label = "";

View file

@ -73,12 +73,29 @@ public class LogUtils {
this.logTag = logTag;
}
public boolean isVerboseLoggable() { return DEBUG || Log.isLoggable(logTag, Log.VERBOSE); }
public boolean isDebugLoggable() { return DEBUG || Log.isLoggable(logTag, Log.DEBUG); }
public boolean isInfoLoggable() { return DEBUG || Log.isLoggable(logTag, Log.INFO); }
public boolean isWarnLoggable() { return DEBUG || Log.isLoggable(logTag, Log.WARN); }
public boolean isErrorLoggable() { return DEBUG || Log.isLoggable(logTag, Log.ERROR); }
public boolean isWtfLoggable() { return DEBUG || Log.isLoggable(logTag, Log.ASSERT); }
public boolean isVerboseLoggable() {
return DEBUG || Log.isLoggable(logTag, Log.VERBOSE);
}
public boolean isDebugLoggable() {
return DEBUG || Log.isLoggable(logTag, Log.DEBUG);
}
public boolean isInfoLoggable() {
return DEBUG || Log.isLoggable(logTag, Log.INFO);
}
public boolean isWarnLoggable() {
return DEBUG || Log.isLoggable(logTag, Log.WARN);
}
public boolean isErrorLoggable() {
return DEBUG || Log.isLoggable(logTag, Log.ERROR);
}
public boolean isWtfLoggable() {
return DEBUG || Log.isLoggable(logTag, Log.ASSERT);
}
public void v(String message, Object... args) {
if (isVerboseLoggable()) {

View file

@ -16,6 +16,9 @@
package com.best.deskclock;
import static com.best.deskclock.AnimatorUtils.getAlphaAnimator;
import static com.best.deskclock.AnimatorUtils.getScaleAnimator;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@ -26,9 +29,6 @@ import android.view.animation.Interpolator;
import com.best.deskclock.uidata.UiDataModel;
import static com.best.deskclock.AnimatorUtils.getAlphaAnimator;
import static com.best.deskclock.AnimatorUtils.getScaleAnimator;
/**
* This runnable chooses a random initial position for {@link #mSaverView} within
* {@link #mContentView} if {@link #mSaverView} is transparent. It also schedules itself to run
@ -37,33 +37,52 @@ import static com.best.deskclock.AnimatorUtils.getScaleAnimator;
*/
public final class MoveScreensaverRunnable implements Runnable {
/** The duration over which the fade in/out animations occur. */
/**
* The duration over which the fade in/out animations occur.
*/
private static final long FADE_TIME = 3000L;
/** Accelerate the hide animation. */
/**
* Accelerate the hide animation.
*/
private final Interpolator mAcceleration = new AccelerateInterpolator();
/** Decelerate the show animation. */
/**
* Decelerate the show animation.
*/
private final Interpolator mDeceleration = new DecelerateInterpolator();
/** The container that houses {@link #mSaverView}. */
/**
* The container that houses {@link #mSaverView}.
*/
private final View mContentView;
/** The display within the {@link #mContentView} that is randomly positioned. */
/**
* The display within the {@link #mContentView} that is randomly positioned.
*/
private final View mSaverView;
/** Tracks the currently executing animation if any; used to gracefully stop the animation. */
/**
* Tracks the currently executing animation if any; used to gracefully stop the animation.
*/
private Animator mActiveAnimator;
/**
* @param contentView contains the {@code saverView}
* @param saverView a child view of {@code contentView} that periodically moves around
* @param saverView a child view of {@code contentView} that periodically moves around
*/
public MoveScreensaverRunnable(View contentView, View saverView) {
mContentView = contentView;
mSaverView = saverView;
}
/**
* @return a random integer between 0 and the {@code maximum} exclusive.
*/
private static float getRandomPoint(float maximum) {
return (int) (Math.random() * maximum);
}
/**
* Start or restart the random movement of the saver view within the content view.
*/
@ -113,7 +132,6 @@ public final class MoveScreensaverRunnable implements Runnable {
mActiveAnimator = getAlphaAnimator(mSaverView, 0f, 1f);
mActiveAnimator.setDuration(FADE_TIME);
mActiveAnimator.setInterpolator(mDeceleration);
mActiveAnimator.start();
} else {
// Select a new random position anywhere in mContentView that will fit mSaverView.
final float newX = getRandomPoint(mContentView.getWidth() - mSaverView.getWidth());
@ -144,14 +162,7 @@ public final class MoveScreensaverRunnable implements Runnable {
final AnimatorSet all = new AnimatorSet();
all.play(show).after(hide);
mActiveAnimator = all;
mActiveAnimator.start();
}
}
/**
* @return a random integer between 0 and the {@code maximum} exclusive.
*/
private static float getRandomPoint(float maximum) {
return (int) (Math.random() * maximum);
mActiveAnimator.start();
}
}

View file

@ -16,7 +16,6 @@
package com.best.deskclock;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW;
@ -24,48 +23,41 @@ import android.app.NotificationChannel;
import android.content.Context;
import android.util.ArraySet;
import android.util.Log;
import androidx.core.app.NotificationManagerCompat;
import com.best.deskclock.Utils;
import androidx.core.app.NotificationManagerCompat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class NotificationUtils {
private static final String TAG = NotificationUtils.class.getSimpleName();
/**
* Notification channel containing all missed alarm notifications.
*/
public static final String ALARM_MISSED_NOTIFICATION_CHANNEL_ID = "alarmMissedNotification";
/**
* Notification channel containing all upcoming alarm notifications.
*/
public static final String ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID = "alarmUpcomingNotification";
/**
* Notification channel containing all snooze notifications.
*/
public static final String ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID = "alarmSnoozingNotification";
/**
* Notification channel containing all firing alarm and timer notifications.
*/
public static final String FIRING_NOTIFICATION_CHANNEL_ID = "firingAlarmsAndTimersNotification";
/**
* Notification channel containing all TimerModel notifications.
*/
public static final String TIMER_MODEL_NOTIFICATION_CHANNEL_ID = "timerNotification";
/**
* Notification channel containing all stopwatch notifications.
*/
public static final String STOPWATCH_NOTIFICATION_CHANNEL_ID = "stopwatchNotification";
private static final String TAG = NotificationUtils.class.getSimpleName();
/**
* Values used to bitmask certain channel defaults
*/
@ -73,7 +65,8 @@ public class NotificationUtils {
private static final int ENABLE_LIGHTS = 0x02;
private static final int ENABLE_VIBRATION = 0x04;
private static Map<String, int[]> CHANNEL_PROPS = new HashMap<String, int[]>();
private static final Map<String, int[]> CHANNEL_PROPS = new HashMap<String, int[]>();
static {
CHANNEL_PROPS.put(ALARM_MISSED_NOTIFICATION_CHANNEL_ID, new int[]{
R.string.alarm_missed_channel,
@ -103,7 +96,7 @@ public class NotificationUtils {
}
public static void createChannel(Context context, String id) {
if (!Utils.isOOrLater()) {
if (Utils.isOOrLater()) {
return;
}
@ -112,8 +105,8 @@ public class NotificationUtils {
return;
}
int[] properties = (int[]) CHANNEL_PROPS.get(id);
int nameId = properties[0];
int[] properties = CHANNEL_PROPS.get(id);
int nameId = Objects.requireNonNull(properties)[0];
int importance = properties[1];
NotificationChannel channel = new NotificationChannel(
id, context.getString(nameId), importance);
@ -145,7 +138,7 @@ public class NotificationUtils {
}
public static void updateNotificationChannels(Context context) {
if (!Utils.isOOrLater()) {
if (Utils.isOOrLater()) {
return;
}

View file

@ -28,8 +28,6 @@ package com.best.deskclock;
*/
public interface Predicate<T> {
boolean apply(T t);
/**
* An implementation of the predicate interface that always returns true.
*/
@ -39,7 +37,6 @@ public interface Predicate<T> {
return true;
}
};
/**
* An implementation of the predicate interface that always returns false.
*/
@ -49,4 +46,6 @@ public interface Predicate<T> {
return false;
}
};
boolean apply(T t);
}

View file

@ -45,10 +45,6 @@ public final class Screensaver extends DreamService {
private String mDateFormatForAccessibility;
private View mContentView;
private View mMainClockView;
private TextClock mDigitalClock;
private AnalogClock mAnalogClock;
/* Register ContentObserver to see alarm changes for pre-L */
private final ContentObserver mSettingsContentObserver =
Utils.isLOrLater() ? null : new ContentObserver(new Handler()) {
@ -57,7 +53,6 @@ public final class Screensaver extends DreamService {
Utils.refreshAlarm(Screensaver.this, mContentView);
}
};
// Runs every midnight or when the time changes and refreshes the date.
private final Runnable mMidnightUpdater = new Runnable() {
@Override
@ -65,7 +60,6 @@ public final class Screensaver extends DreamService {
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
}
};
/**
* Receiver to alarm clock changes.
*/
@ -75,6 +69,9 @@ public final class Screensaver extends DreamService {
Utils.refreshAlarm(Screensaver.this, mContentView);
}
};
private View mMainClockView;
private TextClock mDigitalClock;
private AnalogClock mAnalogClock;
@Override
public void onCreate() {
@ -96,8 +93,8 @@ public final class Screensaver extends DreamService {
mContentView = findViewById(R.id.saver_container);
mMainClockView = mContentView.findViewById(R.id.main_clock);
mDigitalClock = (TextClock) mMainClockView.findViewById(R.id.digital_clock);
mAnalogClock = (AnalogClock) mMainClockView.findViewById(R.id.analog_clock);
mDigitalClock = mMainClockView.findViewById(R.id.digital_clock);
mAnalogClock = mMainClockView.findViewById(R.id.analog_clock);
setClockStyle();
Utils.setClockIconTypeface(mContentView);
@ -123,7 +120,6 @@ public final class Screensaver extends DreamService {
}
if (mSettingsContentObserver != null) {
@SuppressWarnings("deprecation")
final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
getContentResolver().registerContentObserver(uri, false, mSettingsContentObserver);
}

View file

@ -16,6 +16,9 @@
package com.best.deskclock;
import static android.content.Intent.ACTION_BATTERY_CHANGED;
import static android.os.BatteryManager.EXTRA_PLUGGED;
import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -35,21 +38,22 @@ import android.widget.TextClock;
import com.best.deskclock.events.Events;
import com.best.deskclock.uidata.UiDataModel;
import static android.content.Intent.ACTION_BATTERY_CHANGED;
import static android.os.BatteryManager.EXTRA_PLUGGED;
public class ScreensaverActivity extends BaseActivity {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("ScreensaverActivity");
/** These flags keep the screen on if the device is plugged in. */
/**
* These flags keep the screen on if the device is plugged in.
*/
private static final int WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
private final OnPreDrawListener mStartPositionUpdater = new StartPositionUpdater();
private String mDateFormat;
private String mDateFormatForAccessibility;
private View mContentView;
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -71,17 +75,15 @@ public class ScreensaverActivity extends BaseActivity {
}
}
};
/* Register ContentObserver to see alarm changes for pre-L */
private final ContentObserver mSettingsContentObserver = Utils.isPreL()
? new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
}
? new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
}
: null;
}
: null;
// Runs every midnight or when the time changes and refreshes the date.
private final Runnable mMidnightUpdater = new Runnable() {
@Override
@ -89,11 +91,6 @@ public class ScreensaverActivity extends BaseActivity {
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
}
};
private String mDateFormat;
private String mDateFormatForAccessibility;
private View mContentView;
private View mMainClockView;
private MoveScreensaverRunnable mPositionUpdater;
@ -111,7 +108,7 @@ public class ScreensaverActivity extends BaseActivity {
final View digitalClock = mMainClockView.findViewById(R.id.digital_clock);
final AnalogClock analogClock =
(AnalogClock) mMainClockView.findViewById(R.id.analog_clock);
mMainClockView.findViewById(R.id.analog_clock);
Utils.setClockIconTypeface(mMainClockView);
Utils.setTimeFormat((TextClock) digitalClock, false);
@ -149,7 +146,6 @@ public class ScreensaverActivity extends BaseActivity {
registerReceiver(mIntentReceiver, filter);
if (mSettingsContentObserver != null) {
@SuppressWarnings("deprecation")
final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
getContentResolver().registerContentObserver(uri, false, mSettingsContentObserver);
}

View file

@ -16,15 +16,15 @@
package com.best.deskclock;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import android.content.Context;
import android.widget.TextView;
import com.best.deskclock.uidata.UiDataModel;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
/**
* A controller which will format a provided time in millis to display as a stopwatch.
*/

View file

@ -21,12 +21,15 @@ import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
public final class ThemeUtils {
/** Temporary array used internally to resolve attributes. */
/**
* Temporary array used internally to resolve attributes.
*/
private static final int[] TEMP_ATTR = new int[1];
private ThemeUtils() {

View file

@ -43,7 +43,7 @@ public class TimerCircleFrameLayout extends FrameLayout {
* Note: this method assumes the parent container will specify {@link MeasureSpec#EXACTLY exact}
* width and height values.
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent
* @param widthMeasureSpec horizontal space requirements as imposed by the parent
* @param heightMeasureSpec vertical space requirements as imposed by the parent
*/
@Override

View file

@ -16,12 +16,12 @@
package com.best.deskclock;
import android.widget.TextView;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
import android.widget.TextView;
/**
* A controller which will format a provided time in millis to display as a timer.
*/

View file

@ -16,6 +16,13 @@
package com.best.deskclock;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY;
import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.graphics.Bitmap.Config.ARGB_8888;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.AlarmManager;
@ -37,14 +44,6 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.provider.Settings;
import androidx.annotation.AnyRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import androidx.core.os.BuildCompat;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
@ -58,6 +57,15 @@ import android.view.View;
import android.widget.TextClock;
import android.widget.TextView;
import androidx.annotation.AnyRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.best.deskclock.data.DataModel;
import com.best.deskclock.provider.AlarmInstance;
import com.best.deskclock.uidata.UiDataModel;
@ -70,13 +78,6 @@ import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY;
import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.graphics.Bitmap.Config.ARGB_8888;
public class Utils {
/**
@ -146,21 +147,21 @@ public class Utils {
* @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later
*/
public static boolean isNOrLater() {
return BuildCompat.isAtLeastN();
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
/**
* @return {@code true} if the device is {@link Build.VERSION_CODES#N_MR1} or later
*/
public static boolean isNMR1OrLater() {
return BuildCompat.isAtLeastNMR1();
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1;
}
/**
* @return {@code true} if the device is {@link Build.VERSION_CODES#O} or later
*/
public static boolean isOOrLater() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O;
}
/**
@ -294,8 +295,6 @@ public class Utils {
return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context);
}
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.KITKAT)
private static String getNextAlarmPreL(Context context) {
final ContentResolver cr = context.getContentResolver();
return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED);
@ -335,8 +334,8 @@ public class Utils {
* Clock views can call this to refresh their alarm to the next upcoming value.
*/
public static void refreshAlarm(Context context, View clock) {
final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon);
final TextView nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm);
final TextView nextAlarmIconView = clock.findViewById(R.id.nextAlarmIcon);
final TextView nextAlarmView = clock.findViewById(R.id.nextAlarm);
if (nextAlarmView == null) {
return;
}
@ -356,7 +355,7 @@ public class Utils {
}
public static void setClockIconTypeface(View clock) {
final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon);
final TextView nextAlarmIconView = clock.findViewById(R.id.nextAlarmIcon);
nextAlarmIconView.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
}
@ -364,7 +363,7 @@ public class Utils {
* Clock views can call this to refresh their date.
**/
public static void updateDate(String dateSkeleton, String descriptionSkeleton, View clock) {
final TextView dateDisplay = (TextView) clock.findViewById(R.id.date);
final TextView dateDisplay = clock.findViewById(R.id.date);
if (dateDisplay == null) {
return;
}
@ -551,15 +550,15 @@ public class Utils {
}
/**
* @param context to obtain strings.
* @param displayMinutes whether or not minutes should be included
* @param isAhead {@code true} if the time should be marked 'ahead', else 'behind'
* @param hoursDifferent the number of hours the time is ahead/behind
* @param context to obtain strings.
* @param displayMinutes whether or not minutes should be included
* @param isAhead {@code true} if the time should be marked 'ahead', else 'behind'
* @param hoursDifferent the number of hours the time is ahead/behind
* @param minutesDifferent the number of minutes the time is ahead/behind
* @return String describing the hours/minutes ahead or behind
*/
public static String createHoursDifferentString(Context context, boolean displayMinutes,
boolean isAhead, int hoursDifferent, int minutesDifferent) {
boolean isAhead, int hoursDifferent, int minutesDifferent) {
String timeString;
if (displayMinutes && hoursDifferent != 0) {
// Both minutes and hours
@ -590,7 +589,7 @@ public class Utils {
/**
* @param context The context from which to obtain strings
* @param hours Hours to display (if any)
* @param hours Hours to display (if any)
* @param minutes Minutes to display (if any)
* @param seconds Seconds to display
* @return Provided time formatted as a String
@ -607,10 +606,14 @@ public class Utils {
public static final class ClickAccessibilityDelegate extends AccessibilityDelegateCompat {
/** The label for talkback to apply to the view */
/**
* The label for talkback to apply to the view
*/
private final String mLabel;
/** Whether or not to always make the view visible to talkback */
/**
* Whether or not to always make the view visible to talkback
*/
private final boolean mIsAlwaysAccessibilityVisible;
public ClickAccessibilityDelegate(String label) {
@ -623,7 +626,7 @@ public class Utils {
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
if (mIsAlwaysAccessibilityVisible) {
info.setVisibleToUser(true);

View file

@ -17,11 +17,12 @@
package com.best.deskclock;
import android.content.Context;
import androidx.viewpager.widget.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.viewpager.widget.ViewPager;
public class VerticalViewPager extends ViewPager {
public VerticalViewPager(Context context) {

View file

@ -27,17 +27,16 @@ import java.util.List;
public final class MenuItemControllerFactory {
private static final MenuItemControllerFactory INSTANCE = new MenuItemControllerFactory();
public static MenuItemControllerFactory getInstance() {
return INSTANCE;
}
private final List<MenuItemProvider> mMenuItemProviders;
private MenuItemControllerFactory() {
mMenuItemProviders = new ArrayList<>();
}
public static MenuItemControllerFactory getInstance() {
return INSTANCE;
}
public MenuItemControllerFactory addMenuItemProvider(MenuItemProvider provider) {
mMenuItemProviders.add(provider);
return this;

View file

@ -15,6 +15,8 @@
*/
package com.best.deskclock.actionbarmenu;
import static android.view.Menu.NONE;
import android.content.Context;
import android.content.Intent;
import android.view.Menu;
@ -24,8 +26,6 @@ import com.best.deskclock.R;
import com.best.deskclock.ScreensaverActivity;
import com.best.deskclock.events.Events;
import static android.view.Menu.NONE;
/**
* {@link MenuItemController} for controlling night mode display.
*/

View file

@ -17,7 +17,6 @@
package com.best.deskclock.actionbarmenu;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
@ -36,7 +35,7 @@ public final class OptionsMenuManager {
/**
* Add one or more {@link MenuItemController} to the actionbar menu.
* <p/>
* This should be called in {@link Activity#onCreate(Bundle)}.
* This should be called in {link Activity#onCreate(Bundle)}.
*/
public OptionsMenuManager addMenuItemController(MenuItemController... controllers) {
Collections.addAll(mControllers, controllers);

View file

@ -16,20 +16,21 @@
package com.best.deskclock.actionbarmenu;
import static android.view.Menu.FIRST;
import static android.view.Menu.NONE;
import android.content.Context;
import android.os.Bundle;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.SearchView.OnQueryTextListener;
import android.text.InputType;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import com.best.deskclock.R;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.SearchView.OnQueryTextListener;
import static android.view.Menu.FIRST;
import static android.view.Menu.NONE;
import com.best.deskclock.R;
/**
* {@link MenuItemController} for search menu.
@ -49,7 +50,7 @@ public final class SearchMenuItemController implements MenuItemController {
private boolean mSearchMode;
public SearchMenuItemController(Context context, OnQueryTextListener queryListener,
Bundle savedState) {
Bundle savedState) {
mContext = context;
mSearchModeChangeListener = new SearchModeChangeListener();
mQueryListener = queryListener;

View file

@ -16,6 +16,8 @@
package com.best.deskclock.actionbarmenu;
import static android.view.Menu.NONE;
import android.app.Activity;
import android.content.Intent;
import android.view.Menu;
@ -24,8 +26,6 @@ import android.view.MenuItem;
import com.best.deskclock.R;
import com.best.deskclock.settings.SettingsActivity;
import static android.view.Menu.NONE;
/**
* {@link MenuItemController} for settings menu.
*/

View file

@ -15,6 +15,8 @@
*/
package com.best.deskclock.alarms;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@ -37,9 +39,6 @@ import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.animation.PathInterpolatorCompat;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
@ -50,6 +49,10 @@ import android.widget.ImageView;
import android.widget.TextClock;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.animation.PathInterpolatorCompat;
import com.best.deskclock.AnimatorUtils;
import com.best.deskclock.BaseActivity;
import com.best.deskclock.LogUtils;
@ -58,34 +61,62 @@ import com.best.deskclock.ThemeUtils;
import com.best.deskclock.Utils;
import com.best.deskclock.data.DataModel;
import com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
import com.best.deskclock.data.DataModel.ThemeButtonBehavior;
import com.best.deskclock.events.Events;
import com.best.deskclock.provider.AlarmInstance;
import com.best.deskclock.widget.CircleView;
import java.util.List;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
public class AlarmActivity extends BaseActivity
implements View.OnClickListener, View.OnTouchListener {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AlarmActivity");
private static final TimeInterpolator PULSE_INTERPOLATOR =
PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f);
private static final TimeInterpolator REVEAL_INTERPOLATOR =
PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f);
private static final int PULSE_DURATION_MILLIS = 1000;
private static final int ALARM_BOUNCE_DURATION_MILLIS = 500;
private static final int ALERT_REVEAL_DURATION_MILLIS = 500;
private static final int ALERT_FADE_DURATION_MILLIS = 500;
private static final int ALERT_DISMISS_DELAY_MILLIS = 2000;
private static final float BUTTON_SCALE_DEFAULT = 0.7f;
private static final int BUTTON_DRAWABLE_ALPHA_DEFAULT = 165;
private final Handler mHandler = new Handler();
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
LOGGER.i("Finished binding to AlarmService");
}
@Override
public void onServiceDisconnected(ComponentName name) {
LOGGER.i("Disconnected from AlarmService");
}
};
private ThemeButtonBehavior mThemeBehavior;
private AlarmInstance mAlarmInstance;
private boolean mAlarmHandled;
private AlarmVolumeButtonBehavior mVolumeBehavior;
private AlarmVolumeButtonBehavior mPowerBehavior;
private int mCurrentHourColor;
private boolean mReceiverRegistered;
/**
* Whether the AlarmService is currently bound
*/
private boolean mServiceBound;
private AccessibilityManager mAccessibilityManager;
private ViewGroup mAlertView;
private TextView mAlertTitleView;
private TextView mAlertInfoView;
private ViewGroup mContentView;
private ImageView mAlarmButton;
private ImageView mSnoozeButton;
private ImageView mDismissButton;
private TextView mHintView;
private ValueAnimator mAlarmAnimator;
private ValueAnimator mSnoozeAnimator;
private ValueAnimator mDismissAnimator;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -112,49 +143,36 @@ public class AlarmActivity extends BaseActivity
}
}
};
private final ServiceConnection mConnection = new ServiceConnection() {
private final BroadcastReceiver PowerBtnReceiver = new BroadcastReceiver() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
LOGGER.i("Finished binding to AlarmService");
}
@Override
public void onServiceDisconnected(ComponentName name) {
LOGGER.i("Disconnected from AlarmService");
public void onReceive(Context context, Intent intent) {
if (intent != null && intent.getAction() != null) {
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)
|| intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
// Power keys dismiss the alarm.
if (!mAlarmHandled) {
if (mPowerBehavior == AlarmVolumeButtonBehavior.SNOOZE) {
snooze();
} else if (mPowerBehavior == AlarmVolumeButtonBehavior.DISMISS) {
dismiss();
}
}
}
}
}
};
private AlarmInstance mAlarmInstance;
private boolean mAlarmHandled;
private AlarmVolumeButtonBehavior mVolumeBehavior;
private AlarmVolumeButtonBehavior mPowerBehavior;
private int mCurrentHourColor;
private boolean mReceiverRegistered;
/** Whether the AlarmService is currently bound */
private boolean mServiceBound;
private AccessibilityManager mAccessibilityManager;
private ViewGroup mAlertView;
private TextView mAlertTitleView;
private TextView mAlertInfoView;
private ViewGroup mContentView;
private ImageView mAlarmButton;
private ImageView mSnoozeButton;
private ImageView mDismissButton;
private TextView mHintView;
private ValueAnimator mAlarmAnimator;
private ValueAnimator mSnoozeAnimator;
private ValueAnimator mDismissAnimator;
private ValueAnimator mPulseAnimator;
private int mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
@Override
protected void onCreate(Bundle savedInstanceState) {
mThemeBehavior = DataModel.getDataModel().getThemeButtonBehavior();
if (mThemeBehavior == DataModel.ThemeButtonBehavior.DARK) {
getTheme().applyStyle(R.style.Theme_DeskClock_Wallpaper_Dark, true);
}
if (mThemeBehavior == DataModel.ThemeButtonBehavior.LIGHT) {
getTheme().applyStyle(R.style.Theme_DeskClock_Wallpaper_Light, true);
}
super.onCreate(savedInstanceState);
// Register Power button (screen off) intent receiver
@ -181,8 +199,8 @@ public class AlarmActivity extends BaseActivity
// Get the volume/camera button behavior setting
mVolumeBehavior = DataModel.getDataModel().getAlarmVolumeButtonBehavior();
// Get the power button behavior setting
// Get the power button behavior setting
mPowerBehavior = DataModel.getDataModel().getAlarmPowerButtonBehavior();
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
@ -206,19 +224,21 @@ public class AlarmActivity extends BaseActivity
setContentView(R.layout.alarm_activity);
mAlertView = (ViewGroup) findViewById(R.id.alert);
mAlertTitleView = (TextView) mAlertView.findViewById(R.id.alert_title);
mAlertInfoView = (TextView) mAlertView.findViewById(R.id.alert_info);
mAlertView = findViewById(R.id.alert);
mAlertTitleView = mAlertView.findViewById(R.id.alert_title);
mAlertInfoView = mAlertView.findViewById(R.id.alert_info);
mContentView = (ViewGroup) findViewById(R.id.content);
mAlarmButton = (ImageView) mContentView.findViewById(R.id.alarm);
mSnoozeButton = (ImageView) mContentView.findViewById(R.id.snooze);
mDismissButton = (ImageView) mContentView.findViewById(R.id.dismiss);
mHintView = (TextView) mContentView.findViewById(R.id.hint);
mContentView = findViewById(R.id.content);
mAlarmButton = mContentView.findViewById(R.id.alarm);
mSnoozeButton = mContentView.findViewById(R.id.snooze);
mDismissButton = mContentView.findViewById(R.id.dismiss);
mHintView = mContentView.findViewById(R.id.hint);
mDismissButton.setColorFilter(com.google.android.material.R.attr.colorOnBackground);
mSnoozeButton.setColorFilter(com.google.android.material.R.attr.colorOnBackground);
final TextView titleView = (TextView) mContentView.findViewById(R.id.title);
final TextClock digitalClock = (TextClock) mContentView.findViewById(R.id.digital_clock);
final CircleView pulseView = (CircleView) mContentView.findViewById(R.id.pulse);
final TextView titleView = mContentView.findViewById(R.id.title);
final TextClock digitalClock = mContentView.findViewById(R.id.digital_clock);
final CircleView pulseView = mContentView.findViewById(R.id.pulse);
titleView.setText(mAlarmInstance.getLabelOrDefault(this));
Utils.setTimeFormat(digitalClock, false);
@ -231,8 +251,8 @@ public class AlarmActivity extends BaseActivity
mDismissButton.setOnClickListener(this);
mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f);
mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE);
mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor);
mSnoozeAnimator = getButtonAnimator(mSnoozeButton, com.google.android.material.R.attr.colorOnBackground);
mDismissAnimator = getButtonAnimator(mDismissButton, com.google.android.material.R.attr.colorOnBackground);
mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView,
PropertyValuesHolder.ofFloat(CircleView.RADIUS, 0.0f, pulseView.getRadius()),
PropertyValuesHolder.ofObject(CircleView.FILL_COLOR, AnimatorUtils.ARGB_EVALUATOR,
@ -291,28 +311,6 @@ public class AlarmActivity extends BaseActivity
}
}
private final BroadcastReceiver PowerBtnReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && intent.getAction() != null) {
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)
|| intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
// Power keys dismiss the alarm.
if (!mAlarmHandled) {
if (mPowerBehavior == AlarmVolumeButtonBehavior.SNOOZE) {
snooze();
}
else if (mPowerBehavior == AlarmVolumeButtonBehavior.DISMISS){
dismiss();
}
}
}
}
}
};
@Override
public boolean dispatchKeyEvent(@NonNull KeyEvent keyEvent) {
// Do this in dispatch to intercept a few of the system keys.
@ -518,7 +516,7 @@ public class AlarmActivity extends BaseActivity
mAlarmHandled = true;
LOGGER.v("Snoozed: %s", mAlarmInstance);
final int colorAccent = ThemeUtils.resolveColor(this, R.attr.colorPrimaryDark);
final int colorAccent = ThemeUtils.resolveColor(this, androidx.appcompat.R.attr.colorPrimaryDark);
setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
final int snoozeMinutes = DataModel.getDataModel().getSnoozeLength();
@ -621,9 +619,9 @@ public class AlarmActivity extends BaseActivity
}
private Animator getAlertAnimator(final View source, final int titleResId,
final String infoText, final String accessibilityText, final int revealColor,
final int backgroundColor) {
final ViewGroup containerView = (ViewGroup) findViewById(android.R.id.content);
final String infoText, final String accessibilityText, final int revealColor,
final int backgroundColor) {
final ViewGroup containerView = findViewById(android.R.id.content);
final Rect sourceBounds = new Rect(0, 0, source.getHeight(), source.getWidth());
containerView.offsetDescendantRectToMyCoords(source, sourceBounds);

View file

@ -38,7 +38,8 @@ final class AlarmKlaxon {
private static boolean sStarted = false;
private static AsyncRingtonePlayer sAsyncRingtonePlayer;
private AlarmKlaxon() {}
private AlarmKlaxon() {
}
public static void stop(Context context) {
if (sStarted) {

View file

@ -22,7 +22,6 @@ import static com.best.deskclock.NotificationUtils.FIRING_NOTIFICATION_CHANNEL_I
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
@ -31,9 +30,9 @@ import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.service.notification.StatusBarNotification;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import com.best.deskclock.AlarmClockFragment;
import com.best.deskclock.AlarmUtils;
@ -61,54 +60,54 @@ public final class AlarmNotifications {
/**
* This value is coordinated with group ids from
* {@link com.best.deskclock.data.NotificationModel}
* {link com.best.deskclock.data.NotificationModel}
*/
private static final String UPCOMING_GROUP_KEY = "1";
/**
* This value is coordinated with group ids from
* {@link com.best.deskclock.data.NotificationModel}
* {link com.best.deskclock.data.NotificationModel}
*/
private static final String MISSED_GROUP_KEY = "4";
/**
* This value is coordinated with notification ids from
* {@link com.best.deskclock.data.NotificationModel}
* {link com.best.deskclock.data.NotificationModel}
*/
private static final int ALARM_GROUP_NOTIFICATION_ID = Integer.MAX_VALUE - 4;
/**
* This value is coordinated with notification ids from
* {@link com.best.deskclock.data.NotificationModel}
* {link com.best.deskclock.data.NotificationModel}
*/
private static final int ALARM_GROUP_MISSED_NOTIFICATION_ID = Integer.MAX_VALUE - 5;
/**
* This value is coordinated with notification ids from
* {@link com.best.deskclock.data.NotificationModel}
* {link com.best.deskclock.data.NotificationModel}
*/
private static final int ALARM_FIRING_NOTIFICATION_ID = Integer.MAX_VALUE - 7;
static synchronized void showUpcomingNotification(Context context,
AlarmInstance instance, boolean lowPriority) {
AlarmInstance instance, boolean lowPriority) {
LogUtils.v("Displaying upcoming alarm notification for alarm instance: " + instance.mId +
"low priority: " + lowPriority);
NotificationCompat.Builder builder = new NotificationCompat.Builder(
context, ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID)
.setShowWhen(false)
.setContentTitle(context.getString(
R.string.alarm_alert_predismiss_title))
.setContentText(AlarmUtils.getAlarmText(
context, instance, true /* includeLabel */))
.setColor(ContextCompat.getColor(context, R.color.default_background))
.setSmallIcon(R.drawable.stat_notify_alarm)
.setAutoCancel(false)
.setSortKey(createSortKey(instance))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true);
context, ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID)
.setShowWhen(false)
.setContentTitle(context.getString(
R.string.alarm_alert_predismiss_title))
.setContentText(AlarmUtils.getAlarmText(
context, instance, true /* includeLabel */))
.setColor(android.R.attr.colorAccent)
.setSmallIcon(R.drawable.stat_notify_alarm)
.setAutoCancel(false)
.setSortKey(createSortKey(instance))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true);
if (Utils.isNOrLater()) {
builder.setGroup(UPCOMING_GROUP_KEY);
@ -157,15 +156,15 @@ public final class AlarmNotifications {
* extra parameters are needed due to a race condition which exists in
* {@link NotificationManager#getActiveNotifications()}.
*
* @param context Context from which to grab the NotificationManager
* @param group The group key to query for notifications
* @param context Context from which to grab the NotificationManager
* @param group The group key to query for notifications
* @param canceledNotificationId The id of the just-canceled notification (-1 if none)
* @param postedNotification The notification that was just posted
* @param postedNotification The notification that was just posted
* @return The first active notification for the group
*/
@TargetApi(Build.VERSION_CODES.N)
private static Notification getFirstActiveNotification(Context context, String group,
int canceledNotificationId, Notification postedNotification) {
int canceledNotificationId, Notification postedNotification) {
final NotificationManager nm =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final StatusBarNotification[] notifications = nm.getActiveNotifications();
@ -199,7 +198,7 @@ public final class AlarmNotifications {
}
private static void updateUpcomingAlarmGroupNotification(Context context,
int canceledNotificationId, Notification postedNotification) {
int canceledNotificationId, Notification postedNotification) {
if (!Utils.isNOrLater()) {
return;
}
@ -217,10 +216,10 @@ public final class AlarmNotifications {
|| !Objects.equals(summary.contentIntent, firstUpcoming.contentIntent)) {
NotificationUtils.createChannel(context, ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID);
summary = new NotificationCompat.Builder(context,
ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID)
ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID)
.setShowWhen(false)
.setContentIntent(firstUpcoming.contentIntent)
.setColor(ContextCompat.getColor(context, R.color.default_background))
.setColor(android.R.attr.colorAccent)
.setSmallIcon(R.drawable.stat_notify_alarm)
.setGroup(UPCOMING_GROUP_KEY)
.setGroupSummary(true)
@ -234,7 +233,7 @@ public final class AlarmNotifications {
}
private static void updateMissedAlarmGroupNotification(Context context,
int canceledNotificationId, Notification postedNotification) {
int canceledNotificationId, Notification postedNotification) {
if (!Utils.isNOrLater()) {
return;
}
@ -254,7 +253,7 @@ public final class AlarmNotifications {
summary = new NotificationCompat.Builder(context, ALARM_MISSED_NOTIFICATION_CHANNEL_ID)
.setShowWhen(false)
.setContentIntent(firstMissed.contentIntent)
.setColor(ContextCompat.getColor(context, R.color.default_background))
.setColor(android.R.attr.colorAccent)
.setSmallIcon(R.drawable.stat_notify_alarm)
.setGroup(MISSED_GROUP_KEY)
.setGroupSummary(true)
@ -268,23 +267,23 @@ public final class AlarmNotifications {
}
static synchronized void showSnoozeNotification(Context context,
AlarmInstance instance) {
AlarmInstance instance) {
LogUtils.v("Displaying snoozed notification for alarm instance: " + instance.mId);
NotificationCompat.Builder builder = new NotificationCompat.Builder(
context, ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID)
.setShowWhen(false)
.setContentTitle(instance.getLabelOrDefault(context))
.setContentText(context.getString(R.string.alarm_alert_snooze_until,
AlarmUtils.getFormattedTime(context, instance.getAlarmTime())))
.setColor(ContextCompat.getColor(context, R.color.default_background))
.setSmallIcon(R.drawable.stat_notify_alarm)
.setAutoCancel(false)
.setSortKey(createSortKey(instance))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true);
.setShowWhen(false)
.setContentTitle(instance.getLabelOrDefault(context))
.setContentText(context.getString(R.string.alarm_alert_snooze_until,
AlarmUtils.getFormattedTime(context, instance.getAlarmTime())))
.setColor(android.R.attr.colorAccent)
.setSmallIcon(R.drawable.stat_notify_alarm)
.setAutoCancel(false)
.setSortKey(createSortKey(instance))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true);
if (Utils.isNOrLater()) {
builder.setGroup(UPCOMING_GROUP_KEY);
@ -312,24 +311,24 @@ public final class AlarmNotifications {
}
static synchronized void showMissedNotification(Context context,
AlarmInstance instance) {
AlarmInstance instance) {
LogUtils.v("Displaying missed notification for alarm instance: " + instance.mId);
String label = instance.mLabel;
String alarmTime = AlarmUtils.getFormattedTime(context, instance.getAlarmTime());
NotificationCompat.Builder builder = new NotificationCompat.Builder(
context, ALARM_MISSED_NOTIFICATION_CHANNEL_ID)
.setShowWhen(false)
.setContentTitle(context.getString(R.string.alarm_missed_title))
.setContentText(instance.mLabel.isEmpty() ? alarmTime :
context.getString(R.string.alarm_missed_text, alarmTime, label))
.setColor(ContextCompat.getColor(context, R.color.default_background))
.setSortKey(createSortKey(instance))
.setSmallIcon(R.drawable.stat_notify_alarm)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true);
.setShowWhen(false)
.setContentTitle(context.getString(R.string.alarm_missed_title))
.setContentText(instance.mLabel.isEmpty() ? alarmTime :
context.getString(R.string.alarm_missed_text, alarmTime, label))
.setColor(android.R.attr.colorAccent)
.setSortKey(createSortKey(instance))
.setSmallIcon(R.drawable.stat_notify_alarm)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true);
if (Utils.isNOrLater()) {
builder.setGroup(MISSED_GROUP_KEY);
@ -364,18 +363,18 @@ public final class AlarmNotifications {
Resources resources = service.getResources();
NotificationCompat.Builder notification = new NotificationCompat.Builder(
service, FIRING_NOTIFICATION_CHANNEL_ID)
.setContentTitle(instance.getLabelOrDefault(service))
.setContentText(AlarmUtils.getFormattedTime(
service, instance.getAlarmTime()))
.setColor(ContextCompat.getColor(service, R.color.default_background))
.setSmallIcon(R.drawable.stat_notify_alarm)
.setOngoing(true)
.setAutoCancel(false)
.setDefaults(NotificationCompat.DEFAULT_LIGHTS)
.setWhen(0)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true);
.setContentTitle(instance.getLabelOrDefault(service))
.setContentText(AlarmUtils.getFormattedTime(
service, instance.getAlarmTime()))
.setColor(android.R.attr.colorAccent)
.setSmallIcon(R.drawable.stat_notify_alarm)
.setOngoing(true)
.setAutoCancel(false)
.setDefaults(NotificationCompat.DEFAULT_LIGHTS)
.setWhen(0)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true);
// Setup Snooze Action
Intent snoozeIntent = AlarmStateManager.createStateChangeIntent(service,
@ -410,7 +409,7 @@ public final class AlarmNotifications {
fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_NO_USER_ACTION);
notification.setFullScreenIntent(PendingIntent.getActivity(service,
ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE),
ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE),
true);
notification.setPriority(NotificationCompat.PRIORITY_HIGH);

View file

@ -40,7 +40,7 @@ import com.best.deskclock.provider.AlarmInstance;
/**
* This service is in charge of starting/stopping the alarm. It will bring up and manage the
* {@link AlarmActivity} as well as {@link AlarmKlaxon}.
*
* <p>
* Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver
* exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents.
*/
@ -58,13 +58,19 @@ public class AlarmService extends Service {
*/
public static final String ALARM_DISMISS_ACTION = "com.best.deskclock.ALARM_DISMISS";
/** A public action sent by AlarmService when the alarm has started. */
/**
* A public action sent by AlarmService when the alarm has started.
*/
public static final String ALARM_ALERT_ACTION = "com.best.deskclock.ALARM_ALERT";
/** A public action sent by AlarmService when the alarm has stopped for any reason. */
/**
* A public action sent by AlarmService when the alarm has stopped for any reason.
*/
public static final String ALARM_DONE_ACTION = "com.best.deskclock.ALARM_DONE";
/** Private action used to stop an alarm with this service. */
/**
* Private action used to stop an alarm with this service.
*/
public static final String STOP_ALARM_ACTION = "STOP_ALARM";
// constants for no action/snooze/dismiss
@ -75,17 +81,177 @@ public class AlarmService extends Service {
// default action for flip and shake
private static final String DEFAULT_ACTION = Integer.toString(ALARM_NO_ACTION);
/** Binder given to AlarmActivity. */
/**
* Binder given to AlarmActivity.
*/
private final IBinder mBinder = new Binder();
/** Whether the service is currently bound to AlarmActivity */
private boolean mIsBound = false;
/** Listener for changes in phone state. */
/**
* Listener for changes in phone state.
*/
private final PhoneStateChangeListener mPhoneStateListener = new PhoneStateChangeListener();
/** Whether the receiver is currently registered */
/**
* Whether the service is currently bound to AlarmActivity
*/
private boolean mIsBound = false;
/**
* Whether the receiver is currently registered
*/
private boolean mIsRegistered = false;
private TelephonyManager mTelephonyManager;
private AlarmInstance mCurrentAlarm = null;
private final BroadcastReceiver mActionsReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
LogUtils.i("AlarmService received intent %s", action);
if (mCurrentAlarm == null || mCurrentAlarm.mAlarmState != AlarmInstance.FIRED_STATE) {
LogUtils.i("No valid firing alarm");
return;
}
if (mIsBound) {
LogUtils.i("AlarmActivity bound; AlarmService no-op");
return;
}
switch (action) {
case ALARM_SNOOZE_ACTION:
// Set the alarm state to snoozed.
// If this broadcast receiver is handling the snooze intent then AlarmActivity
// must not be showing, so always show snooze toast.
AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */);
Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
break;
case ALARM_DISMISS_ACTION:
// Set the alarm state to dismissed.
AlarmStateManager.deleteInstanceAndUpdateParent(context, mCurrentAlarm);
Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
break;
}
}
};
private SensorManager mSensorManager;
private int mFlipAction;
private final ResettableSensorEventListener mFlipListener =
new ResettableSensorEventListener() {
// Accelerometers are not quite accurate.
private static final float GRAVITY_UPPER_THRESHOLD = 1.3f * SensorManager.STANDARD_GRAVITY;
private static final float GRAVITY_LOWER_THRESHOLD = 0.7f * SensorManager.STANDARD_GRAVITY;
private static final int SENSOR_SAMPLES = 3;
private final boolean[] mSamples = new boolean[SENSOR_SAMPLES];
private boolean mStopped;
private boolean mWasFaceUp;
private int mSampleIndex;
@Override
public void onAccuracyChanged(Sensor sensor, int acc) {
}
@Override
public void reset() {
mWasFaceUp = false;
mStopped = false;
for (int i = 0; i < SENSOR_SAMPLES; i++) {
mSamples[i] = false;
}
}
private boolean filterSamples() {
boolean allPass = true;
for (boolean sample : mSamples) {
allPass = allPass && sample;
}
return allPass;
}
@Override
public void onSensorChanged(SensorEvent event) {
// Add a sample overwriting the oldest one. Several samples
// are used to avoid the erroneous values the sensor sometimes
// returns.
float z = event.values[2];
if (mStopped) {
return;
}
if (!mWasFaceUp) {
// Check if its face up enough.
mSamples[mSampleIndex] = (z > GRAVITY_LOWER_THRESHOLD) &&
(z < GRAVITY_UPPER_THRESHOLD);
// face up
if (filterSamples()) {
mWasFaceUp = true;
for (int i = 0; i < SENSOR_SAMPLES; i++) {
mSamples[i] = false;
}
}
} else {
// Check if its face down enough.
mSamples[mSampleIndex] = (z < -GRAVITY_LOWER_THRESHOLD) &&
(z > -GRAVITY_UPPER_THRESHOLD);
// face down
if (filterSamples()) {
mStopped = true;
handleAction(mFlipAction);
}
}
mSampleIndex = ((mSampleIndex + 1) % SENSOR_SAMPLES);
}
};
private int mShakeAction;
private final SensorEventListener mShakeListener = new SensorEventListener() {
private static final float SENSITIVITY = 16;
private static final int BUFFER = 5;
private final float[] gravity = new float[3];
private float average = 0;
private int fill = 0;
@Override
public void onAccuracyChanged(Sensor sensor, int acc) {
}
public void onSensorChanged(SensorEvent event) {
final float alpha = 0.8F;
for (int i = 0; i < 3; i++) {
gravity[i] = alpha * gravity[i] + (1 - alpha) * event.values[i];
}
float x = event.values[0] - gravity[0];
float y = event.values[1] - gravity[1];
float z = event.values[2] - gravity[2];
if (fill <= BUFFER) {
average += Math.abs(x) + Math.abs(y) + Math.abs(z);
fill++;
} else {
if (average / BUFFER >= SENSITIVITY) {
handleAction(mShakeAction);
}
average = 0;
fill = 0;
}
}
};
/**
* Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
* or using a different instance.
*
* @param context application context
* @param instance you are trying to stop
*/
public static void stopAlarm(Context context, AlarmInstance instance) {
final Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId)
.setAction(STOP_ALARM_ACTION);
// We don't need a wake lock here, since we are trying to kill an alarm
context.startService(intent);
}
@Override
public IBinder onBind(Intent intent) {
@ -99,27 +265,6 @@ public class AlarmService extends Service {
return super.onUnbind(intent);
}
/**
* Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
* or using a different instance.
*
* @param context application context
* @param instance you are trying to stop
*/
public static void stopAlarm(Context context, AlarmInstance instance) {
final Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId)
.setAction(STOP_ALARM_ACTION);
// We don't need a wake lock here, since we are trying to kill an alarm
context.startService(intent);
}
private TelephonyManager mTelephonyManager;
private AlarmInstance mCurrentAlarm = null;
private SensorManager mSensorManager;
private int mFlipAction;
private int mShakeAction;
private void startAlarm(AlarmInstance instance) {
LogUtils.v("AlarmService.start with instance: " + instance.mId);
if (mCurrentAlarm != null) {
@ -157,38 +302,6 @@ public class AlarmService extends Service {
AlarmAlertWakeLock.releaseCpuLock();
}
private final BroadcastReceiver mActionsReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
LogUtils.i("AlarmService received intent %s", action);
if (mCurrentAlarm == null || mCurrentAlarm.mAlarmState != AlarmInstance.FIRED_STATE) {
LogUtils.i("No valid firing alarm");
return;
}
if (mIsBound) {
LogUtils.i("AlarmActivity bound; AlarmService no-op");
return;
}
switch (action) {
case ALARM_SNOOZE_ACTION:
// Set the alarm state to snoozed.
// If this broadcast receiver is handling the snooze intent then AlarmActivity
// must not be showing, so always show snooze toast.
AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */);
Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
break;
case ALARM_DISMISS_ACTION:
// Set the alarm state to dismissed.
AlarmStateManager.deleteInstanceAndUpdateParent(context, mCurrentAlarm);
Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
break;
}
}
};
@Override
public void onCreate() {
super.onCreate();
@ -266,139 +379,6 @@ public class AlarmService extends Service {
}
}
private final class PhoneStateChangeListener extends PhoneStateListener {
private int mPhoneCallState;
PhoneStateChangeListener init() {
mPhoneCallState = -1;
return this;
}
@Override
public void onCallStateChanged(int state, String ignored) {
if (mPhoneCallState == -1) {
mPhoneCallState = state;
}
if (state != TelephonyManager.CALL_STATE_IDLE && state != mPhoneCallState) {
startService(AlarmStateManager.createStateChangeIntent(AlarmService.this,
"AlarmService", mCurrentAlarm, AlarmInstance.MISSED_STATE));
}
}
}
private interface ResettableSensorEventListener extends SensorEventListener {
public void reset();
}
private final ResettableSensorEventListener mFlipListener =
new ResettableSensorEventListener() {
// Accelerometers are not quite accurate.
private static final float GRAVITY_UPPER_THRESHOLD = 1.3f * SensorManager.STANDARD_GRAVITY;
private static final float GRAVITY_LOWER_THRESHOLD = 0.7f * SensorManager.STANDARD_GRAVITY;
private static final int SENSOR_SAMPLES = 3;
private boolean mStopped;
private boolean mWasFaceUp;
private boolean[] mSamples = new boolean[SENSOR_SAMPLES];
private int mSampleIndex;
@Override
public void onAccuracyChanged(Sensor sensor, int acc) {
}
@Override
public void reset() {
mWasFaceUp = false;
mStopped = false;
for (int i = 0; i < SENSOR_SAMPLES; i++) {
mSamples[i] = false;
}
}
private boolean filterSamples() {
boolean allPass = true;
for (boolean sample : mSamples) {
allPass = allPass && sample;
}
return allPass;
}
@Override
public void onSensorChanged(SensorEvent event) {
// Add a sample overwriting the oldest one. Several samples
// are used to avoid the erroneous values the sensor sometimes
// returns.
float z = event.values[2];
if (mStopped) {
return;
}
if (!mWasFaceUp) {
// Check if its face up enough.
mSamples[mSampleIndex] = (z > GRAVITY_LOWER_THRESHOLD) &&
(z < GRAVITY_UPPER_THRESHOLD);
// face up
if (filterSamples()) {
mWasFaceUp = true;
for (int i = 0; i < SENSOR_SAMPLES; i++) {
mSamples[i] = false;
}
}
} else {
// Check if its face down enough.
mSamples[mSampleIndex] = (z < -GRAVITY_LOWER_THRESHOLD) &&
(z > -GRAVITY_UPPER_THRESHOLD);
// face down
if (filterSamples()) {
mStopped = true;
handleAction(mFlipAction);
}
}
mSampleIndex = ((mSampleIndex + 1) % SENSOR_SAMPLES);
}
};
private final SensorEventListener mShakeListener = new SensorEventListener() {
private static final float SENSITIVITY = 16;
private static final int BUFFER = 5;
private float[] gravity = new float[3];
private float average = 0;
private int fill = 0;
@Override
public void onAccuracyChanged(Sensor sensor, int acc) {
}
public void onSensorChanged(SensorEvent event) {
final float alpha = 0.8F;
for (int i = 0; i < 3; i++) {
gravity[i] = alpha * gravity[i] + (1 - alpha) * event.values[i];
}
float x = event.values[0] - gravity[0];
float y = event.values[1] - gravity[1];
float z = event.values[2] - gravity[2];
if (fill <= BUFFER) {
average += Math.abs(x) + Math.abs(y) + Math.abs(z);
fill++;
} else {
if (average / BUFFER >= SENSITIVITY) {
handleAction(mShakeAction);
}
average = 0;
fill = 0;
}
}
};
private void attachListeners() {
if (mFlipAction != ALARM_NO_ACTION) {
mFlipListener.reset();
@ -444,4 +424,30 @@ public class AlarmService extends Service {
break;
}
}
private interface ResettableSensorEventListener extends SensorEventListener {
void reset();
}
private final class PhoneStateChangeListener extends PhoneStateListener {
private int mPhoneCallState;
PhoneStateChangeListener init() {
mPhoneCallState = -1;
return this;
}
@Override
public void onCallStateChanged(int state, String ignored) {
if (mPhoneCallState == -1) {
mPhoneCallState = state;
}
if (state != TelephonyManager.CALL_STATE_IDLE && state != mPhoneCallState) {
startService(AlarmStateManager.createStateChangeIntent(AlarmService.this,
"AlarmService", mCurrentAlarm, AlarmInstance.MISSED_STATE));
}
}
}
}

View file

@ -15,6 +15,9 @@
*/
package com.best.deskclock.alarms;
import static android.content.Context.ALARM_SERVICE;
import static android.provider.Settings.System.NEXT_ALARM_FORMATTED;
import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.AlarmManager.AlarmClockInfo;
@ -28,10 +31,11 @@ import android.os.Build;
import android.os.Handler;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.core.app.NotificationManagerCompat;
import android.text.format.DateFormat;
import android.widget.Toast;
import androidx.core.app.NotificationManagerCompat;
import com.best.deskclock.AlarmAlertWakeLock;
import com.best.deskclock.AlarmClockFragment;
import com.best.deskclock.AlarmUtils;
@ -49,55 +53,53 @@ import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static android.content.Context.ALARM_SERVICE;
import static android.provider.Settings.System.NEXT_ALARM_FORMATTED;
import java.util.Objects;
/**
* This class handles all the state changes for alarm instances. You need to
* register all alarm instances with the state manager if you want them to
* be activated. If a major time change has occurred (ie. TIMEZONE_CHANGE, TIMESET_CHANGE),
* then you must also re-register instances to fix their states.
*
* <p>
* Please see {@link #registerInstance) for special transitions when major time changes
* occur.
*
* <p>
* Following states:
*
* <p>
* SILENT_STATE:
* This state is used when the alarm is activated, but doesn't need to display anything. It
* is in charge of changing the alarm instance state to a LOW_NOTIFICATION_STATE.
*
* <p>
* LOW_NOTIFICATION_STATE:
* This state is used to notify the user that the alarm will go off
* {@link AlarmInstance#LOW_NOTIFICATION_HOUR_OFFSET}. This
* state handles the state changes to HIGH_NOTIFICATION_STATE, HIDE_NOTIFICATION_STATE and
* DISMISS_STATE.
*
* <p>
* HIDE_NOTIFICATION_STATE:
* This is a transient state of the LOW_NOTIFICATION_STATE, where the user wants to hide the
* notification. This will sit and wait until the HIGH_PRIORITY_NOTIFICATION should go off.
*
* <p>
* HIGH_NOTIFICATION_STATE:
* This state behaves like the LOW_NOTIFICATION_STATE, but doesn't allow the user to hide it.
* This state is in charge of triggering a FIRED_STATE or DISMISS_STATE.
*
* <p>
* SNOOZED_STATE:
* The SNOOZED_STATE behaves like a HIGH_NOTIFICATION_STATE, but with a different message. It
* also increments the alarm time in the instance to reflect the new snooze time.
*
* <p>
* FIRED_STATE:
* The FIRED_STATE is used when the alarm is firing. It will start the AlarmService, and wait
* until the user interacts with the alarm via SNOOZED_STATE or DISMISS_STATE change. If the user
* doesn't then it might be change to MISSED_STATE if auto-silenced was enabled.
*
* <p>
* MISSED_STATE:
* The MISSED_STATE is used when the alarm already fired, but the user could not interact with
* it. At this point the alarm instance is dead and we check the parent alarm to see if we need
* to disable or schedule a new alarm_instance. There is also a notification shown to the user
* that he/she missed the alarm and that stays for
* {@link AlarmInstance#MISSED_TIME_TO_LIVE_HOUR_OFFSET} or until the user acknownledges it.
*
* <p>
* DISMISS_STATE:
* This is really a transient state that will properly delete the alarm instance. Use this state,
* whenever you want to get rid of the alarm instance. This state will also check the alarm
@ -109,51 +111,37 @@ public final class AlarmStateManager extends BroadcastReceiver {
// Intent action to show the alarm and dismiss the instance
public static final String SHOW_AND_DISMISS_ALARM_ACTION = "show_and_dismiss_alarm";
// Intent action for an AlarmManager alarm serving only to set the next alarm indicators
private static final String INDICATOR_ACTION = "indicator";
// System intent action to notify AppWidget that we changed the alarm text.
public static final String ACTION_ALARM_CHANGED = "com.best.deskclock.ALARM_CHANGED";
// Extra key to set the desired state change.
public static final String ALARM_STATE_EXTRA = "intent.extra.alarm.state";
// Extra key to indicate the state change was launched from a notification.
public static final String FROM_NOTIFICATION_EXTRA = "intent.extra.from.notification";
// Extra key to set the global broadcast id.
private static final String ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id";
// Intent category tags used to dismiss, snooze or delete an alarm
public static final String ALARM_DISMISS_TAG = "DISMISS_TAG";
public static final String ALARM_SNOOZE_TAG = "SNOOZE_TAG";
public static final String ALARM_DELETE_TAG = "DELETE_TAG";
// Intent category tag used when schedule state change intents in alarm manager.
private static final String ALARM_MANAGER_TAG = "ALARM_MANAGER";
// Buffer time in seconds to fire alarm instead of marking it missed.
public static final int ALARM_FIRE_BUFFER = 15;
// Intent action for an AlarmManager alarm serving only to set the next alarm indicators
private static final String INDICATOR_ACTION = "indicator";
// Extra key to set the global broadcast id.
private static final String ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id";
// Intent category tag used when schedule state change intents in alarm manager.
private static final String ALARM_MANAGER_TAG = "ALARM_MANAGER";
private static final String ACTION_SET_POWEROFF_ALARM =
"org.codeaurora.poweroffalarm.action.SET_ALARM";
private static final String ACTION_CANCEL_POWEROFF_ALARM =
"org.codeaurora.poweroffalarm.action.CANCEL_ALARM";
private static final String POWER_OFF_ALARM_PACKAGE =
"com.qualcomm.qti.poweroffalarm";
private static final String TIME = "time";
// A factory for the current time; can be mocked for testing purposes.
private static CurrentTimeFactory sCurrentTimeFactory;
// Schedules alarm state transitions; can be mocked for testing purposes.
private static StateChangeScheduler sStateChangeScheduler =
new AlarmManagerStateChangeScheduler();
private static final String ACTION_SET_POWEROFF_ALARM =
"org.codeaurora.poweroffalarm.action.SET_ALARM";
private static final String ACTION_CANCEL_POWEROFF_ALARM =
"org.codeaurora.poweroffalarm.action.CANCEL_ALARM";
private static final String POWER_OFF_ALARM_PACKAGE =
"com.qualcomm.qti.poweroffalarm";
private static final String TIME = "time";
private static Calendar getCurrentTime() {
return sCurrentTimeFactory == null
? DataModel.getDataModel().getCalendar()
@ -212,8 +200,6 @@ public final class AlarmStateManager extends BroadcastReceiver {
/**
* Used in pre-L devices, where "next alarm" is stored in system settings.
*/
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.KITKAT)
private static void updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm) {
// Format the next alarm time if an alarm is scheduled.
String time = "";
@ -225,13 +211,13 @@ public final class AlarmStateManager extends BroadcastReceiver {
// Write directly to NEXT_ALARM_FORMATTED in all pre-L versions
Settings.System.putString(context.getContentResolver(), NEXT_ALARM_FORMATTED, time);
LogUtils.i("Updated next alarm time to: \'" + time + '\'');
LogUtils.i("Updated next alarm time to: '" + time + '\'');
// Send broadcast message so pre-L AppWidgets will recognize an update.
context.sendBroadcast(new Intent(ACTION_ALARM_CHANGED));
} catch (SecurityException se) {
// The user has most likely revoked WRITE_SETTINGS.
LogUtils.e("Unable to update next alarm to: \'" + time + '\'', se);
LogUtils.e("Unable to update next alarm to: '" + time + '\'', se);
}
}
@ -278,7 +264,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
ContentResolver cr = context.getContentResolver();
Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
if (alarm == null) {
LogUtils.e("Parent has been deleted with instance: " + instance.toString());
LogUtils.e("Parent has been deleted with instance: " + instance);
return;
}
@ -318,7 +304,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
* @return intent that can be used to change an alarm instance state
*/
public static Intent createStateChangeIntent(Context context, String tag,
AlarmInstance instance, Integer state) {
AlarmInstance instance, Integer state) {
// This intent is directed to AlarmService, though the actual handling of it occurs here
// in AlarmStateManager. The reason is that evidence exists showing the jump between the
// broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by the
@ -344,7 +330,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
* @param newState to change to
*/
private static void scheduleInstanceStateChange(Context ctx, Calendar time,
AlarmInstance instance, int newState) {
AlarmInstance instance, int newState) {
sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState);
}
@ -490,7 +476,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
* @param instance to set state to
*/
public static void setSnoozeState(final Context context, AlarmInstance instance,
boolean showToast) {
boolean showToast) {
// Stop alarm if this instance is firing it
AlarmService.stopAlarm(context, instance);
@ -518,7 +504,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
@Override
public void run() {
String displayTime = String.format(context.getResources().getQuantityText
(R.plurals.alarm_alert_snooze_set, snoozeMinutes).toString(),
(R.plurals.alarm_alert_snooze_set, snoozeMinutes).toString(),
snoozeMinutes);
Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show();
}
@ -649,7 +635,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
* This registers the AlarmInstance to the state manager. This will look at the instance
* and choose the most appropriate state to put it in. This is primarily used by new
* alarms, but it can also be called when the system time changes.
*
* <p>
* Most state changes are handled by the states themselves, but during major time changes we
* have to correct the alarm instance state. This means we have to handle special cases as
* describe below:
@ -662,7 +648,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
* <li>If alarm was SNOOZED, then show the notification but don't update time</li>
* <li>If low priority notification was hidden, then make sure it stays hidden</li>
* </ul>
*
* <p>
* If none of these special case are found, then we just check the time and see what is the
* proper state for the instance.
*
@ -670,7 +656,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
* @param instance to register
*/
public static void registerInstance(Context context, AlarmInstance instance,
boolean updateNextAlarm) {
boolean updateNextAlarm) {
LogUtils.i("Registering instance: " + instance.mId);
final ContentResolver cr = context.getContentResolver();
final Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
@ -710,7 +696,7 @@ public final class AlarmStateManager extends BroadcastReceiver {
// Make sure we re-enable the parent alarm of the instance
// because it will get activated by by the below code
alarm.enabled = true;
Objects.requireNonNull(alarm).enabled = true;
Alarm.updateAlarm(cr, alarm);
}
} else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) {
@ -897,25 +883,6 @@ public final class AlarmStateManager extends BroadcastReceiver {
}
}
@Override
public void onReceive(final Context context, final Intent intent) {
if (INDICATOR_ACTION.equals(intent.getAction())) {
return;
}
final PendingResult result = goAsync();
final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
wl.acquire();
AsyncHandler.post(new Runnable() {
@Override
public void run() {
handleIntent(context, intent);
result.finish();
wl.release();
}
});
}
public static void handleIntent(Context context, Intent intent) {
final String action = intent.getAction();
LogUtils.v("AlarmStateManager received intent " + intent);
@ -990,6 +957,42 @@ public final class AlarmStateManager extends BroadcastReceiver {
return new Intent(context, AlarmStateManager.class).setAction(INDICATOR_ACTION);
}
private static void setPowerOffAlarm(Context context, AlarmInstance instance) {
LogUtils.i("Set next power off alarm : instance id " + instance.mId);
Intent intent = new Intent(ACTION_SET_POWEROFF_ALARM);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.setPackage(POWER_OFF_ALARM_PACKAGE);
intent.putExtra(TIME, instance.getAlarmTime().getTimeInMillis());
context.sendBroadcast(intent);
}
private static void cancelPowerOffAlarm(Context context, AlarmInstance instance) {
Intent intent = new Intent(ACTION_CANCEL_POWEROFF_ALARM);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(TIME, instance.getAlarmTime().getTimeInMillis());
intent.setPackage(POWER_OFF_ALARM_PACKAGE);
context.sendBroadcast(intent);
}
@Override
public void onReceive(final Context context, final Intent intent) {
if (INDICATOR_ACTION.equals(intent.getAction())) {
return;
}
final PendingResult result = goAsync();
final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
wl.acquire();
AsyncHandler.post(new Runnable() {
@Override
public void run() {
handleIntent(context, intent);
result.finish();
wl.release();
}
});
}
/**
* Abstract away how the current time is computed. If no implementation of this interface is
* given the default is to return {@link Calendar#getInstance()}. Otherwise, the factory
@ -1006,35 +1009,18 @@ public final class AlarmStateManager extends BroadcastReceiver {
*/
interface StateChangeScheduler {
void scheduleInstanceStateChange(Context context, Calendar time,
AlarmInstance instance, int newState);
AlarmInstance instance, int newState);
void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance);
}
private static void setPowerOffAlarm(Context context, AlarmInstance instance) {
LogUtils.i("Set next power off alarm : instance id "+ instance.mId);
Intent intent = new Intent(ACTION_SET_POWEROFF_ALARM);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.setPackage(POWER_OFF_ALARM_PACKAGE);
intent.putExtra(TIME, instance.getAlarmTime().getTimeInMillis());
context.sendBroadcast(intent);
}
private static void cancelPowerOffAlarm(Context context, AlarmInstance instance) {
Intent intent = new Intent(ACTION_CANCEL_POWEROFF_ALARM);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(TIME, instance.getAlarmTime().getTimeInMillis());
intent.setPackage(POWER_OFF_ALARM_PACKAGE);
context.sendBroadcast(intent);
}
/**
* Schedules state change callbacks within the AlarmManager.
*/
private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler {
@Override
public void scheduleInstanceStateChange(Context context, Calendar time,
AlarmInstance instance, int newState) {
AlarmInstance instance, int newState) {
final long timeInMillis = time.getTimeInMillis();
LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState,
instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis);

View file

@ -22,7 +22,6 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.Vibrator;
import com.best.deskclock.AlarmClockFragment;
import com.best.deskclock.LabelDialogFragment;
import com.best.deskclock.LogUtils;
@ -45,19 +44,16 @@ public final class AlarmTimeClickHandler {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AlarmTimeClickHandler");
private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap";
final Vibrator vibrator;
private final Fragment mFragment;
private final Context mContext;
private final AlarmUpdateHandler mAlarmUpdateHandler;
private final ScrollHandler mScrollHandler;
private Alarm mSelectedAlarm;
private Bundle mPreviousDaysOfWeekMap;
final Vibrator vibrator;
public AlarmTimeClickHandler(Fragment fragment, Bundle savedState,
AlarmUpdateHandler alarmUpdateHandler, ScrollHandler smoothScrollController) {
AlarmUpdateHandler alarmUpdateHandler, ScrollHandler smoothScrollController) {
mFragment = fragment;
mContext = mFragment.getActivity().getApplicationContext();
mAlarmUpdateHandler = alarmUpdateHandler;

View file

@ -19,7 +19,6 @@ package com.best.deskclock.alarms;
import android.content.ContentResolver;
import android.content.Context;
import android.os.AsyncTask;
import com.google.android.material.snackbar.Snackbar;
import android.text.format.DateFormat;
import android.view.View;
import android.view.ViewGroup;
@ -30,6 +29,7 @@ import com.best.deskclock.events.Events;
import com.best.deskclock.provider.Alarm;
import com.best.deskclock.provider.AlarmInstance;
import com.best.deskclock.widget.toast.SnackbarManager;
import com.google.android.material.snackbar.Snackbar;
import java.util.Calendar;
import java.util.List;
@ -47,7 +47,7 @@ public final class AlarmUpdateHandler {
private Alarm mDeletedAlarm;
public AlarmUpdateHandler(Context context, ScrollHandler scrollHandler,
ViewGroup snackbarAnchor) {
ViewGroup snackbarAnchor) {
mAppContext = context.getApplicationContext();
mScrollHandler = scrollHandler;
mSnackbarAnchor = snackbarAnchor;
@ -100,7 +100,7 @@ public final class AlarmUpdateHandler {
* @param minorUpdate if true, don't affect any currently snoozed instances.
*/
public void asyncUpdateAlarm(final Alarm alarm, final boolean popToast,
final boolean minorUpdate) {
final boolean minorUpdate) {
final AsyncTask<Void, Void, AlarmInstance> updateTask =
new AsyncTask<Void, Void, AlarmInstance>() {
@Override
@ -200,7 +200,7 @@ public final class AlarmUpdateHandler {
private void showUndoBar() {
final Alarm deletedAlarm = mDeletedAlarm;
final Snackbar snackbar = Snackbar.make(mSnackbarAnchor,
mAppContext.getString(R.string.alarm_deleted), Snackbar.LENGTH_LONG)
mAppContext.getString(R.string.alarm_deleted), Snackbar.LENGTH_LONG)
.setAction(R.string.alarm_undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -214,7 +214,7 @@ public final class AlarmUpdateHandler {
private AlarmInstance setupAlarmInstance(Alarm alarm) {
final ContentResolver cr = mAppContext.getContentResolver();
AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance());
newInstance = AlarmInstance.addInstance(cr, newInstance);
AlarmInstance.addInstance(cr, newInstance);
// Register instance to state manager
AlarmStateManager.registerInstance(mAppContext, newInstance, true);
return newInstance;

View file

@ -24,10 +24,10 @@ import android.app.TimePickerDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import android.text.format.DateFormat;
import android.widget.TimePicker;
import androidx.appcompat.app.AlertDialog;
import com.best.deskclock.Utils;
@ -46,45 +46,6 @@ public class TimePickerDialogFragment extends DialogFragment {
private static final String ARG_HOUR = TAG + "_hour";
private static final String ARG_MINUTE = TAG + "_minute";
@Override
@SuppressWarnings("deprecation")
public Dialog onCreateDialog(Bundle savedInstanceState) {
final OnTimeSetListener listener = ((OnTimeSetListener) getParentFragment());
final Calendar now = Calendar.getInstance();
final Bundle args = getArguments() == null ? Bundle.EMPTY : getArguments();
final int hour = args.getInt(ARG_HOUR, now.get(Calendar.HOUR_OF_DAY));
final int minute = args.getInt(ARG_MINUTE, now.get(Calendar.MINUTE));
if (Utils.isLOrLater()) {
final Context context = getActivity();
return new TimePickerDialog(context, new TimePickerDialog.OnTimeSetListener() {
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
listener.onTimeSet(TimePickerDialogFragment.this, hourOfDay, minute);
}
}, hour, minute, DateFormat.is24HourFormat(context));
} else {
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
final Context context = builder.getContext();
final TimePicker timePicker = new TimePicker(context);
timePicker.setCurrentHour(hour);
timePicker.setCurrentMinute(minute);
timePicker.setIs24HourView(DateFormat.is24HourFormat(context));
return builder.setView(timePicker)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
listener.onTimeSet(TimePickerDialogFragment.this,
timePicker.getCurrentHour(), timePicker.getCurrentMinute());
}
}).setNegativeButton(android.R.string.cancel, null /* listener */)
.create();
}
}
public static void show(Fragment fragment) {
show(fragment, -1 /* hour */, -1 /* minute */);
}
@ -125,6 +86,45 @@ public class TimePickerDialogFragment extends DialogFragment {
}
}
@Override
@SuppressWarnings("deprecation")
public Dialog onCreateDialog(Bundle savedInstanceState) {
final OnTimeSetListener listener = ((OnTimeSetListener) getParentFragment());
final Calendar now = Calendar.getInstance();
final Bundle args = getArguments() == null ? Bundle.EMPTY : getArguments();
final int hour = args.getInt(ARG_HOUR, now.get(Calendar.HOUR_OF_DAY));
final int minute = args.getInt(ARG_MINUTE, now.get(Calendar.MINUTE));
if (Utils.isLOrLater()) {
final Context context = getActivity();
return new TimePickerDialog(context, new TimePickerDialog.OnTimeSetListener() {
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
listener.onTimeSet(TimePickerDialogFragment.this, hourOfDay, minute);
}
}, hour, minute, DateFormat.is24HourFormat(context));
} else {
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
final Context context = builder.getContext();
final TimePicker timePicker = new TimePicker(context);
timePicker.setCurrentHour(hour);
timePicker.setCurrentMinute(minute);
timePicker.setIs24HourView(DateFormat.is24HourFormat(context));
return builder.setView(timePicker)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
listener.onTimeSet(TimePickerDialogFragment.this,
timePicker.getCurrentHour(), timePicker.getCurrentMinute());
}
}).setNegativeButton(android.R.string.cancel, null /* listener */)
.create();
}
}
/**
* The callback interface used to indicate the user is done filling in the time (e.g. they
* clicked on the 'OK' button).

View file

@ -31,7 +31,7 @@ public class AlarmItemHolder extends ItemAdapter.ItemHolder<Alarm> {
private boolean mExpanded;
public AlarmItemHolder(Alarm alarm, AlarmInstance alarmInstance,
AlarmTimeClickHandler alarmTimeClickHandler) {
AlarmTimeClickHandler alarmTimeClickHandler) {
super(alarm, alarm.id);
mAlarmInstance = alarmInstance;
mAlarmTimeClickHandler = alarmTimeClickHandler;

View file

@ -57,11 +57,11 @@ public abstract class AlarmItemViewHolder extends ItemAdapter.ItemViewHolder<Ala
public AlarmItemViewHolder(View itemView) {
super(itemView);
clock = (TextTime) itemView.findViewById(R.id.digital_clock);
onOff = (CompoundButton) itemView.findViewById(R.id.onoff);
arrow = (ImageView) itemView.findViewById(R.id.arrow);
clock = itemView.findViewById(R.id.digital_clock);
onOff = itemView.findViewById(R.id.onoff);
arrow = itemView.findViewById(R.id.arrow);
preemptiveDismissButton =
(TextView) itemView.findViewById(R.id.preemptive_dismiss_button);
itemView.findViewById(R.id.preemptive_dismiss_button);
preemptiveDismissButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -101,13 +101,13 @@ public abstract class AlarmItemViewHolder extends ItemAdapter.ItemViewHolder<Ala
}
protected boolean bindPreemptiveDismissButton(Context context, Alarm alarm,
AlarmInstance alarmInstance) {
AlarmInstance alarmInstance) {
final boolean canBind = alarm.canPreemptivelyDismiss() && alarmInstance != null;
if (canBind) {
preemptiveDismissButton.setVisibility(View.VISIBLE);
final String dismissText = alarm.instanceState == AlarmInstance.SNOOZE_STATE
? context.getString(R.string.alarm_alert_snooze_until,
AlarmUtils.getAlarmText(context, alarmInstance, false))
AlarmUtils.getAlarmText(context, alarmInstance, false))
: context.getString(R.string.alarm_alert_dismiss_text);
preemptiveDismissButton.setText(dismissText);
preemptiveDismissButton.setClickable(true);

View file

@ -22,12 +22,13 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Rect;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.best.deskclock.AnimatorUtils;
import com.best.deskclock.ItemAdapter;
import com.best.deskclock.R;
@ -46,9 +47,8 @@ import java.util.List;
public final class CollapsedAlarmViewHolder extends AlarmItemViewHolder {
public static final int VIEW_TYPE = R.layout.alarm_time_collapsed;
private final TextView alarmLabel;
public final TextView daysOfWeek;
private final TextView alarmLabel;
private final TextView upcomingInstanceLabel;
private final View hairLine;
@ -57,9 +57,9 @@ public final class CollapsedAlarmViewHolder extends AlarmItemViewHolder {
private CollapsedAlarmViewHolder(View itemView) {
super(itemView);
alarmLabel = (TextView) itemView.findViewById(R.id.label);
daysOfWeek = (TextView) itemView.findViewById(R.id.days_of_week);
upcomingInstanceLabel = (TextView) itemView.findViewById(R.id.upcoming_instance_label);
alarmLabel = itemView.findViewById(R.id.label);
daysOfWeek = itemView.findViewById(R.id.days_of_week);
upcomingInstanceLabel = itemView.findViewById(R.id.upcoming_instance_label);
hairLine = itemView.findViewById(R.id.hairline);
// Expand handler
@ -155,14 +155,14 @@ public final class CollapsedAlarmViewHolder extends AlarmItemViewHolder {
@Override
public Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
int fromBottom, long duration) {
int fromBottom, long duration) {
/* There are no possible partial animations for collapsed view holders. */
return null;
}
@Override
public Animator onAnimateChange(final ViewHolder oldHolder, ViewHolder newHolder,
long duration) {
long duration) {
if (!(oldHolder instanceof AlarmItemViewHolder)
|| !(newHolder instanceof AlarmItemViewHolder)) {
return null;

View file

@ -16,19 +16,19 @@
package com.best.deskclock.alarms.dataadapter;
import static android.content.Context.VIBRATOR_SERVICE;
import static android.view.View.TRANSLATION_Y;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Vibrator;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -37,6 +37,9 @@ import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.best.deskclock.AnimatorUtils;
import com.best.deskclock.ItemAdapter;
import com.best.deskclock.R;
@ -51,9 +54,6 @@ import com.best.deskclock.uidata.UiDataModel;
import java.util.List;
import static android.content.Context.VIBRATOR_SERVICE;
import static android.view.View.TRANSLATION_Y;
/**
* A ViewHolder containing views for an alarm item in expanded state.
*/
@ -61,12 +61,12 @@ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
public static final int VIEW_TYPE = R.layout.alarm_time_expanded;
public final CheckBox repeat;
private final TextView editLabel;
public final LinearLayout repeatDays;
private final CompoundButton[] dayButtons = new CompoundButton[7];
public final CheckBox vibrate;
public final TextView ringtone;
public final TextView delete;
private final TextView editLabel;
private final CompoundButton[] dayButtons = new CompoundButton[7];
private final View hairLine;
private final boolean mHasVibrator;
@ -76,18 +76,18 @@ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
mHasVibrator = hasVibrator;
delete = (TextView) itemView.findViewById(R.id.delete);
repeat = (CheckBox) itemView.findViewById(R.id.repeat_onoff);
vibrate = (CheckBox) itemView.findViewById(R.id.vibrate_onoff);
ringtone = (TextView) itemView.findViewById(R.id.choose_ringtone);
editLabel = (TextView) itemView.findViewById(R.id.edit_label);
repeatDays = (LinearLayout) itemView.findViewById(R.id.repeat_days);
delete = itemView.findViewById(R.id.delete);
repeat = itemView.findViewById(R.id.repeat_onoff);
vibrate = itemView.findViewById(R.id.vibrate_onoff);
ringtone = itemView.findViewById(R.id.choose_ringtone);
editLabel = itemView.findViewById(R.id.edit_label);
repeatDays = itemView.findViewById(R.id.repeat_days);
hairLine = itemView.findViewById(R.id.hairline);
final Context context = itemView.getContext();
itemView.setBackground(new LayerDrawable(new Drawable[] {
itemView.setBackground(new LayerDrawable(new Drawable[]{
ContextCompat.getDrawable(context, R.drawable.alarm_background_expanded),
ThemeUtils.resolveDrawable(context, R.attr.selectableItemBackground)
ThemeUtils.resolveDrawable(context, androidx.appcompat.R.attr.selectableItemBackground)
}));
// Build button for each day.
@ -97,7 +97,7 @@ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
final View dayButtonFrame = inflater.inflate(R.layout.day_button, repeatDays,
false /* attachToRoot */);
final CompoundButton dayButton =
(CompoundButton) dayButtonFrame.findViewById(R.id.day_button_box);
dayButtonFrame.findViewById(R.id.day_button_box);
final int weekday = weekdays.get(i);
dayButton.setText(UiDataModel.getUiDataModel().getShortWeekday(weekday));
dayButton.setContentDescription(UiDataModel.getUiDataModel().getLongWeekday(weekday));
@ -108,8 +108,6 @@ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
// Cannot set in xml since we need compat functionality for API < 21
final Drawable labelIcon = Utils.getVectorDrawable(context, R.drawable.ic_label);
editLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(labelIcon, null, null, null);
final Drawable deleteIcon = Utils.getVectorDrawable(context, R.drawable.ic_delete_small);
delete.setCompoundDrawablesRelativeWithIntrinsicBounds(deleteIcon, null, null, null);
// Collapse handler
itemView.setOnClickListener(new View.OnClickListener() {
@ -222,10 +220,11 @@ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
if (alarm.daysOfWeek.isBitOn(weekdays.get(i))) {
dayButton.setChecked(true);
dayButton.setTextColor(ThemeUtils.resolveColor(context,
android.R.attr.colorBackground));
android.R.attr.textColorPrimaryInverse));
} else {
dayButton.setChecked(false);
dayButton.setTextColor(context.getResources().getColor(R.color.day_unchecked_color));
dayButton.setTextColor(ThemeUtils.resolveColor(context,
android.R.attr.textColorPrimary));
}
}
if (alarm.daysOfWeek.isRepeating()) {
@ -259,7 +258,7 @@ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
@Override
public Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
int fromBottom, long duration) {
int fromBottom, long duration) {
if (payloads == null || payloads.isEmpty() || !payloads.contains(ANIMATE_REPEAT_DAYS)) {
return null;
}
@ -272,8 +271,8 @@ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(AnimatorUtils.getBoundsAnimator(itemView,
fromLeft, fromTop, fromRight, fromBottom,
itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom()),
fromLeft, fromTop, fromRight, fromBottom,
itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom()),
ObjectAnimator.ofFloat(repeatDays, View.ALPHA, isExpansion ? 1f : 0f),
ObjectAnimator.ofFloat(repeatDays, TRANSLATION_Y, isExpansion ? 0f : -height),
ObjectAnimator.ofFloat(ringtone, TRANSLATION_Y, 0f),
@ -311,7 +310,7 @@ public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
@Override
public Animator onAnimateChange(final ViewHolder oldHolder, ViewHolder newHolder,
long duration) {
long duration) {
if (!(oldHolder instanceof AlarmItemViewHolder)
|| !(newHolder instanceof AlarmItemViewHolder)) {
return null;

View file

@ -16,15 +16,16 @@
package com.best.deskclock.controller;
import static com.best.deskclock.Utils.enforceMainLooper;
import android.app.Activity;
import android.content.Context;
import androidx.annotation.StringRes;
import com.best.deskclock.Utils;
import com.best.deskclock.events.EventTracker;
import static com.best.deskclock.Utils.enforceMainLooper;
/**
* Interactions with Android framework components responsible for part of the user experience are
* handled via this singleton.
@ -35,16 +36,23 @@ public final class Controller {
private Context mContext;
/** The controller that dispatches app events to event trackers. */
/**
* The controller that dispatches app events to event trackers.
*/
private EventController mEventController;
/** The controller that interacts with voice interaction sessions on M+. */
/**
* The controller that interacts with voice interaction sessions on M+.
*/
private VoiceController mVoiceController;
/** The controller that creates and updates launcher shortcuts on N MR1+ */
/**
* The controller that creates and updates launcher shortcuts on N MR1+
*/
private ShortcutController mShortcutController;
private Controller() {}
private Controller() {
}
public static Controller getController() {
return sController;
@ -86,8 +94,8 @@ public final class Controller {
* events such as button presses or other user interactions with your application.
*
* @param category resource id of event category
* @param action resource id of event action
* @param label resource id of event label
* @param action resource id of event action
* @param label resource id of event label
*/
public void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
mEventController.sendEvent(category, action, label);

View file

@ -26,6 +26,7 @@ import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.UserManager;
import android.provider.AlarmClock;
import androidx.annotation.StringRes;
import com.best.deskclock.DeskClock;

View file

@ -33,7 +33,7 @@ class VoiceController {
* command was processed successfully.
*
* @param activity an Activity that may be hosting a voice interaction session
* @param message to be spoken to the user to indicate success
* @param message to be spoken to the user to indicate success
*/
void notifyVoiceSuccess(Activity activity, String message) {
if (!Utils.isMOrLater()) {
@ -52,7 +52,7 @@ class VoiceController {
* command failed and must be aborted.
*
* @param activity an Activity that may be hosting a voice interaction session
* @param message to be spoken to the user to indicate failure
* @param message to be spoken to the user to indicate failure
*/
void notifyVoiceFailure(Activity activity, String message) {
if (!Utils.isMOrLater()) {

View file

@ -31,10 +31,14 @@ import com.best.deskclock.provider.Alarm;
*/
final class AlarmModel {
/** The model from which settings are fetched. */
/**
* The model from which settings are fetched.
*/
private final SettingsModel mSettingsModel;
/** The uri of the default ringtone to play for new alarms; mirrors last selection. */
/**
* The uri of the default ringtone to play for new alarms; mirrors last selection.
*/
private Uri mDefaultAlarmRingtoneUri;
AlarmModel(Context context, SettingsModel settingsModel) {
@ -73,7 +77,7 @@ final class AlarmModel {
AlarmVolumeButtonBehavior getAlarmPowerButtonBehavior() {
return mSettingsModel.getAlarmPowerButtonBehavior();
}
int getAlarmTimeout() {
return mSettingsModel.getAlarmTimeout();
}

View file

@ -16,6 +16,8 @@
package com.best.deskclock.data;
import androidx.annotation.NonNull;
import java.text.Collator;
import java.util.Comparator;
import java.util.Locale;
@ -27,25 +29,39 @@ import java.util.TimeZone;
*/
public final class City {
/** A unique identifier for the city. */
/**
* A unique identifier for the city.
*/
private final String mId;
/** An optional numeric index used to order cities for display; -1 if no such index exists. */
/**
* An optional numeric index used to order cities for display; -1 if no such index exists.
*/
private final int mIndex;
/** An index string used to order cities for display. */
/**
* An index string used to order cities for display.
*/
private final String mIndexString;
/** The display name of the city. */
/**
* The display name of the city.
*/
private final String mName;
/** The phonetic name of the city used to order cities for display. */
/**
* The phonetic name of the city used to order cities for display.
*/
private final String mPhoneticName;
/** The TimeZone corresponding to the city. */
/**
* The TimeZone corresponding to the city.
*/
private final TimeZone mTimeZone;
/** A cached upper case form of the {@link #mName} used in case-insensitive name comparisons. */
/**
* A cached upper case form of the {@link #mName} used in case-insensitive name comparisons.
*/
private String mNameUpperCase;
/**
@ -63,12 +79,40 @@ public final class City {
mTimeZone = tz;
}
public String getId() { return mId; }
public int getIndex() { return mIndex; }
public String getName() { return mName; }
public TimeZone getTimeZone() { return mTimeZone; }
public String getIndexString() { return mIndexString; }
public String getPhoneticName() { return mPhoneticName; }
/**
* Strips out any characters considered optional for matching purposes. These include spaces,
* dashes, periods and apostrophes.
*
* @param token a city name or search term
* @return the given {@code token} without any characters considered optional when matching
*/
public static String removeSpecialCharacters(String token) {
return token.replaceAll("[ -.']", "");
}
public String getId() {
return mId;
}
public int getIndex() {
return mIndex;
}
public String getName() {
return mName;
}
public TimeZone getTimeZone() {
return mTimeZone;
}
public String getIndexString() {
return mIndexString;
}
public String getPhoneticName() {
return mPhoneticName;
}
/**
* @return the city name converted to upper case
@ -92,7 +136,7 @@ public final class City {
/**
* @param upperCaseQueryNoSpecialCharacters search term with all special characters removed
* to match against the upper case city name
* to match against the upper case city name
* @return {@code true} iff the name of this city starts with the given query
*/
public boolean matches(String upperCaseQueryNoSpecialCharacters) {
@ -101,6 +145,7 @@ public final class City {
return getNameUpperCaseNoSpecialCharacters().startsWith(upperCaseQueryNoSpecialCharacters);
}
@NonNull
@Override
public String toString() {
return String.format(Locale.US,
@ -108,17 +153,6 @@ public final class City {
mId, mIndex, mIndexString, mName, mPhoneticName, mTimeZone.getID());
}
/**
* Strips out any characters considered optional for matching purposes. These include spaces,
* dashes, periods and apostrophes.
*
* @param token a city name or search term
* @return the given {@code token} without any characters considered optional when matching
*/
public static String removeSpecialCharacters(String token) {
return token.replaceAll("[ -.']", "");
}
/**
* Orders by:
*

View file

@ -20,10 +20,11 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.content.res.TypedArray;
import androidx.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArrayMap;
import androidx.annotation.VisibleForTesting;
import com.best.deskclock.R;
import java.util.ArrayList;
@ -42,16 +43,23 @@ import java.util.regex.Pattern;
*/
final class CityDAO {
/** Regex to match numeric index values when parsing city names. */
/**
* Regex to match numeric index values when parsing city names.
*/
private static final Pattern NUMERIC_INDEX_REGEX = Pattern.compile("\\d+");
/** Key to a preference that stores the number of selected cities. */
/**
* Key to a preference that stores the number of selected cities.
*/
private static final String NUMBER_OF_CITIES = "number_of_cities";
/** Prefix for a key to a preference that stores the id of a selected city. */
/**
* Prefix for a key to a preference that stores the id of a selected city.
*/
private static final String CITY_ID = "city_id_";
private CityDAO() {}
private CityDAO() {
}
/**
* @param cityMap maps city ids to city instances
@ -136,11 +144,11 @@ final class CityDAO {
}
/**
* @param id unique identifier for city
* @param id unique identifier for city
* @param formattedName "[index string]=[name]" or "[index string]=[name]:[phonetic name]",
* If [index string] is empty, use the first character of name as index,
* If phonetic name is empty, use the name itself as phonetic name.
* @param tzId the string id of the timezone a given city is located in
* @param tzId the string id of the timezone a given city is located in
*/
@VisibleForTesting
static City createCity(String id, String formattedName, String tzId) {

View file

@ -46,7 +46,9 @@ final class CityModel {
private final SharedPreferences mPrefs;
/** The model from which settings are fetched. */
/**
* The model from which settings are fetched.
*/
private final SettingsModel mSettingsModel;
/**
@ -56,26 +58,40 @@ final class CityModel {
@SuppressWarnings("FieldCanBeLocal")
private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
/** Clears data structures containing data that is locale-sensitive. */
/**
* Clears data structures containing data that is locale-sensitive.
*/
@SuppressWarnings("FieldCanBeLocal")
private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
/** List of listeners to invoke upon world city list change */
/**
* List of listeners to invoke upon world city list change
*/
private final List<CityListener> mCityListeners = new ArrayList<>();
/** Maps city ID to city instance. */
/**
* Maps city ID to city instance.
*/
private Map<String, City> mCityMap;
/** List of city instances in display order. */
/**
* List of city instances in display order.
*/
private List<City> mAllCities;
/** List of selected city instances in display order. */
/**
* List of selected city instances in display order.
*/
private List<City> mSelectedCities;
/** List of unselected city instances in display order. */
/**
* List of unselected city instances in display order.
*/
private List<City> mUnselectedCities;
/** A city instance representing the home timezone of the user. */
/**
* A city instance representing the home timezone of the user.
*/
private City mHomeCity;
CityModel(Context context, SharedPreferences prefs, SettingsModel settingsModel) {
@ -193,8 +209,10 @@ final class CityModel {
Comparator<City> getCityIndexComparator() {
final CitySort citySort = mSettingsModel.getCitySort();
switch (citySort) {
case NAME: return new City.NameIndexComparator();
case UTC_OFFSET: return new City.UtcOffsetIndexComparator();
case NAME:
return new City.NameIndexComparator();
case UTC_OFFSET:
return new City.UtcOffsetIndexComparator();
}
throw new IllegalStateException("unexpected city sort: " + citySort);
}
@ -228,8 +246,10 @@ final class CityModel {
private Comparator<City> getCitySortComparator() {
final CitySort citySort = mSettingsModel.getCitySort();
switch (citySort) {
case NAME: return new City.NameComparator();
case UTC_OFFSET: return new City.UtcOffsetComparator();
case NAME:
return new City.NameComparator();
case UTC_OFFSET:
return new City.UtcOffsetComparator();
}
throw new IllegalStateException("unexpected city sort: " + citySort);
}

View file

@ -17,6 +17,7 @@
package com.best.deskclock.data;
import android.net.Uri;
import androidx.annotation.NonNull;
/**
@ -24,16 +25,24 @@ import androidx.annotation.NonNull;
*/
public final class CustomRingtone implements Comparable<CustomRingtone> {
/** The unique identifier of the custom ringtone. */
/**
* The unique identifier of the custom ringtone.
*/
private final long mId;
/** The uri that allows playback of the ringtone. */
/**
* The uri that allows playback of the ringtone.
*/
private final Uri mUri;
/** The title describing the file at the given uri; typically the file name. */
/**
* The title describing the file at the given uri; typically the file name.
*/
private final String mTitle;
/** {@code true} iff the application has permission to read the content of {@code mUri uri}. */
/**
* {@code true} iff the application has permission to read the content of {@code mUri uri}.
*/
private final boolean mHasPermissions;
CustomRingtone(long id, Uri uri, String title, boolean hasPermissions) {
@ -43,10 +52,21 @@ public final class CustomRingtone implements Comparable<CustomRingtone> {
mHasPermissions = hasPermissions;
}
public long getId() { return mId; }
public Uri getUri() { return mUri; }
public String getTitle() { return mTitle; }
public boolean hasPermissions() { return mHasPermissions; }
public long getId() {
return mId;
}
public Uri getUri() {
return mUri;
}
public String getTitle() {
return mTitle;
}
public boolean hasPermissions() {
return mHasPermissions;
}
CustomRingtone setHasPermissions(boolean hasPermissions) {
if (mHasPermissions == hasPermissions) {

View file

@ -31,22 +31,31 @@ import java.util.Set;
*/
final class CustomRingtoneDAO {
/** Key to a preference that stores the set of all custom ringtone ids. */
/**
* Key to a preference that stores the set of all custom ringtone ids.
*/
private static final String RINGTONE_IDS = "ringtone_ids";
/** Key to a preference that stores the next unused ringtone id. */
/**
* Key to a preference that stores the next unused ringtone id.
*/
private static final String NEXT_RINGTONE_ID = "next_ringtone_id";
/** Prefix for a key to a preference that stores the URI associated with the ringtone id. */
/**
* Prefix for a key to a preference that stores the URI associated with the ringtone id.
*/
private static final String RINGTONE_URI = "ringtone_uri_";
/** Prefix for a key to a preference that stores the title associated with the ringtone id. */
/**
* Prefix for a key to a preference that stores the title associated with the ringtone id.
*/
private static final String RINGTONE_TITLE = "ringtone_title_";
private CustomRingtoneDAO() {}
private CustomRingtoneDAO() {
}
/**
* @param uri points to an audio file located on the file system
* @param uri points to an audio file located on the file system
* @param title the title of the audio content at the given {@code uri}
* @return the newly added custom ringtone
*/
@ -88,7 +97,7 @@ final class CustomRingtoneDAO {
* @return a list of all known custom ringtones
*/
static List<CustomRingtone> getCustomRingtones(SharedPreferences prefs) {
final Set<String> ids = prefs.getStringSet(RINGTONE_IDS, Collections.<String>emptySet());
final Set<String> ids = prefs.getStringSet(RINGTONE_IDS, Collections.emptySet());
final List<CustomRingtone> ringtones = new ArrayList<>(ids.size());
for (String id : ids) {
@ -102,6 +111,6 @@ final class CustomRingtoneDAO {
}
private static Set<String> getRingtoneIds(SharedPreferences prefs) {
return new HashSet<>(prefs.getStringSet(RINGTONE_IDS, Collections.<String>emptySet()));
return new HashSet<>(prefs.getStringSet(RINGTONE_IDS, Collections.emptySet()));
}
}

View file

@ -16,6 +16,15 @@
package com.best.deskclock.data;
import static android.content.Context.AUDIO_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.media.AudioManager.FLAG_SHOW_UI;
import static android.media.AudioManager.STREAM_ALARM;
import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
import static android.provider.Settings.ACTION_SOUND_SETTINGS;
import static com.best.deskclock.Utils.enforceMainLooper;
import static com.best.deskclock.Utils.enforceNotMainLooper;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
@ -24,9 +33,10 @@ import android.media.AudioManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.StringRes;
import android.view.View;
import androidx.annotation.StringRes;
import com.best.deskclock.Predicate;
import com.best.deskclock.R;
import com.best.deskclock.Utils;
@ -37,169 +47,69 @@ import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import static android.content.Context.AUDIO_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.media.AudioManager.FLAG_SHOW_UI;
import static android.media.AudioManager.STREAM_ALARM;
import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
import static android.provider.Settings.ACTION_SOUND_SETTINGS;
import static com.best.deskclock.Utils.enforceMainLooper;
import static com.best.deskclock.Utils.enforceNotMainLooper;
/**
* All application-wide data is accessible through this singleton.
*/
public final class DataModel {
/** Indicates the display style of clocks. */
public enum ClockStyle {ANALOG, DIGITAL}
/** Indicates the preferred sort order of cities. */
public enum CitySort {NAME, UTC_OFFSET}
/** Indicates the preferred behavior of hardware volume buttons when firing alarms. */
public enum AlarmVolumeButtonBehavior {NOTHING, SNOOZE, DISMISS}
/** Indicates the reason alarms may not fire or may fire silently. */
public enum SilentSetting {
@SuppressWarnings("unchecked")
DO_NOT_DISTURB(R.string.alarms_blocked_by_dnd, 0, Predicate.FALSE, null),
@SuppressWarnings("unchecked")
MUTED_VOLUME(R.string.alarm_volume_muted,
R.string.unmute_alarm_volume,
Predicate.TRUE,
new UnmuteAlarmVolumeListener()),
SILENT_RINGTONE(R.string.silent_default_alarm_ringtone,
R.string.change_setting_action,
new ChangeSoundActionPredicate(),
new ChangeSoundSettingsListener()),
@SuppressWarnings("unchecked")
BLOCKED_NOTIFICATIONS(R.string.app_notifications_blocked,
R.string.change_setting_action,
Predicate.TRUE,
new ChangeAppNotificationSettingsListener());
private final @StringRes int mLabelResId;
private final @StringRes int mActionResId;
private final Predicate<Context> mActionEnabled;
private final View.OnClickListener mActionListener;
SilentSetting(int labelResId, int actionResId, Predicate<Context> actionEnabled,
View.OnClickListener actionListener) {
mLabelResId = labelResId;
mActionResId = actionResId;
mActionEnabled = actionEnabled;
mActionListener = actionListener;
}
public @StringRes int getLabelResId() { return mLabelResId; }
public @StringRes int getActionResId() { return mActionResId; }
public View.OnClickListener getActionListener() { return mActionListener; }
public boolean isActionEnabled(Context context) {
return mLabelResId != 0 && mActionEnabled.apply(context);
}
private static class UnmuteAlarmVolumeListener implements View.OnClickListener {
@Override
public void onClick(View v) {
// Set the alarm volume to 11/16th of max and show the slider UI.
// 11/16th of max is the initial volume of the alarm stream on a fresh install.
final Context context = v.getContext();
final AudioManager am = (AudioManager) context.getSystemService(AUDIO_SERVICE);
final int index = Math.round(am.getStreamMaxVolume(STREAM_ALARM) * 11f / 16f);
am.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI);
}
}
private static class ChangeSoundSettingsListener implements View.OnClickListener {
@Override
public void onClick(View v) {
final Context context = v.getContext();
context.startActivity(new Intent(ACTION_SOUND_SETTINGS)
.addFlags(FLAG_ACTIVITY_NEW_TASK));
}
}
private static class ChangeSoundActionPredicate implements Predicate<Context> {
@Override
public boolean apply(Context context) {
final Intent intent = new Intent(ACTION_SOUND_SETTINGS);
return intent.resolveActivity(context.getPackageManager()) != null;
}
}
private static class ChangeAppNotificationSettingsListener implements View.OnClickListener {
@Override
public void onClick(View v) {
final Context context = v.getContext();
if (Utils.isLOrLater()) {
try {
// Attempt to open the notification settings for this app.
context.startActivity(
new Intent("android.settings.APP_NOTIFICATION_SETTINGS")
.putExtra("app_package", context.getPackageName())
.putExtra("app_uid", context.getApplicationInfo().uid)
.addFlags(FLAG_ACTIVITY_NEW_TASK));
return;
} catch (Exception ignored) {
// best attempt only; recovery code below
}
}
// Fall back to opening the app settings page.
context.startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", context.getPackageName(), null))
.addFlags(FLAG_ACTIVITY_NEW_TASK));
}
}
}
public static final String ACTION_WORLD_CITIES_CHANGED =
"com.best.deskclock.WORLD_CITIES_CHANGED";
/** The single instance of this data model that exists for the life of the application. */
/**
* The single instance of this data model that exists for the life of the application.
*/
private static final DataModel sDataModel = new DataModel();
private Handler mHandler;
private Context mContext;
/** The model from which settings are fetched. */
/**
* The model from which settings are fetched.
*/
private SettingsModel mSettingsModel;
/** The model from which city data are fetched. */
/**
* The model from which city data are fetched.
*/
private CityModel mCityModel;
/** The model from which timer data are fetched. */
/**
* The model from which timer data are fetched.
*/
private TimerModel mTimerModel;
/** The model from which alarm data are fetched. */
/**
* The model from which alarm data are fetched.
*/
private AlarmModel mAlarmModel;
/** The model from which widget data are fetched. */
private ThemeModel mThemeModel;
/**
* The model from which widget data are fetched.
*/
private WidgetModel mWidgetModel;
/** The model from which data about settings that silence alarms are fetched. */
/**
* The model from which data about settings that silence alarms are fetched.
*/
private SilentSettingsModel mSilentSettingsModel;
/** The model from which stopwatch data are fetched. */
/**
* The model from which stopwatch data are fetched.
*/
private StopwatchModel mStopwatchModel;
/** The model from which notification data are fetched. */
/**
* The model from which notification data are fetched.
*/
private NotificationModel mNotificationModel;
/** The model from which time data are fetched. */
/**
* The model from which time data are fetched.
*/
private TimeModel mTimeModel;
/** The model from which ringtone data are fetched. */
/**
* The model from which ringtone data are fetched.
*/
private RingtoneModel mRingtoneModel;
private DataModel() {
}
public static DataModel getDataModel() {
return sDataModel;
}
private DataModel() {}
/**
* Initializes the data model with the context and shared preferences to be used.
*/
@ -214,6 +124,7 @@ public final class DataModel {
mSettingsModel = new SettingsModel(mContext, prefs, mTimeModel);
mCityModel = new CityModel(mContext, prefs, mSettingsModel);
mAlarmModel = new AlarmModel(mContext, mSettingsModel);
mThemeModel = new ThemeModel(mContext, mSettingsModel);
mSilentSettingsModel = new SilentSettingsModel(mContext, mNotificationModel);
mStopwatchModel = new StopwatchModel(mContext, prefs, mNotificationModel);
mTimerModel = new TimerModel(mContext, prefs, mSettingsModel, mRingtoneModel,
@ -280,9 +191,13 @@ public final class DataModel {
return mHandler;
}
//
// Application
//
/**
* @return {@code true} when the application is open in the foreground; {@code false} otherwise
*/
public boolean isApplicationInForeground() {
enforceMainLooper();
return mNotificationModel.isApplicationInForeground();
}
/**
* @param inForeground {@code true} to indicate the application is open in the foreground
@ -301,14 +216,6 @@ public final class DataModel {
}
}
/**
* @return {@code true} when the application is open in the foreground; {@code false} otherwise
*/
public boolean isApplicationInForeground() {
enforceMainLooper();
return mNotificationModel.isApplicationInForeground();
}
/**
* Called when the notifications may be stale or absent from the notification manager and must
* be rebuilt. e.g. after upgrading the application
@ -320,10 +227,6 @@ public final class DataModel {
mStopwatchModel.updateNotification();
}
//
// Cities
//
/**
* @return a list of all cities in their display order
*/
@ -332,6 +235,10 @@ public final class DataModel {
return mCityModel.getAllCities();
}
//
// Application
//
/**
* @return a city representing the user's home timezone
*/
@ -356,6 +263,10 @@ public final class DataModel {
return mCityModel.getSelectedCities();
}
//
// Cities
//
/**
* @param cities the new collection of cities selected for display by the user
*/
@ -404,10 +315,6 @@ public final class DataModel {
mCityModel.removeCityListener(cityListener);
}
//
// Timers
//
/**
* @param timerListener to be notified when timers are added, updated and removed
*/
@ -440,6 +347,10 @@ public final class DataModel {
return mTimerModel.getExpiredTimers();
}
//
// Timers
//
/**
* @param timerId identifies the timer to return
* @return the timer with the given {@code timerId}
@ -451,7 +362,7 @@ public final class DataModel {
/**
* @return the timer that last expired and is still expired now; {@code null} if no timers are
* expired
* expired
*/
public Timer getMostRecentExpiredTimer() {
enforceMainLooper();
@ -459,8 +370,8 @@ public final class DataModel {
}
/**
* @param length the length of the timer in milliseconds
* @param label describes the purpose of the timer
* @param length the length of the timer in milliseconds
* @param label describes the purpose of the timer
* @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
* @return the newly added timer
*/
@ -486,7 +397,7 @@ public final class DataModel {
/**
* @param service used to start foreground notifications for expired timers
* @param timer the timer to be started
* @param timer the timer to be started
*/
public void startTimer(Service service, Timer timer) {
enforceMainLooper();
@ -511,7 +422,7 @@ public final class DataModel {
/**
* @param service used to start foreground notifications for expired timers
* @param timer the timer to be expired
* @param timer the timer to be expired
*/
public void expireTimer(Service service, Timer timer) {
enforceMainLooper();
@ -532,7 +443,7 @@ public final class DataModel {
* removes the the timer. The timer is otherwise transitioned to the reset state and continues
* to exist.
*
* @param timer the timer to be reset
* @param timer the timer to be reset
* @param eventLabelId the label of the timer event to send; 0 if no event should be sent
* @return the reset {@code timer} or {@code null} if the timer was deleted
*/
@ -589,7 +500,7 @@ public final class DataModel {
}
/**
* @param timer the timer whose {@code length} to change
* @param timer the timer whose {@code length} to change
* @param length the new length of the timer in milliseconds
*/
public void setTimerLength(Timer timer, long length) {
@ -598,7 +509,7 @@ public final class DataModel {
}
/**
* @param timer the timer whose {@code remainingTime} to change
* @param timer the timer whose {@code remainingTime} to change
* @param remainingTime the new remaining time of the timer in milliseconds
*/
public void setRemainingTime(Timer timer, long remainingTime) {
@ -661,7 +572,7 @@ public final class DataModel {
/**
* @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
* {@code 0} implies no crescendo should be applied
* {@code 0} implies no crescendo should be applied
*/
public long getTimerCrescendoDuration() {
enforceMainLooper();
@ -684,10 +595,6 @@ public final class DataModel {
mTimerModel.setTimerVibrate(enabled);
}
//
// Alarms
//
/**
* @return the uri of the ringtone to which all new alarms default
*/
@ -706,7 +613,7 @@ public final class DataModel {
/**
* @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
* {@code 0} implies no crescendo should be applied
* {@code 0} implies no crescendo should be applied
*/
public long getAlarmCrescendoDuration() {
enforceMainLooper();
@ -721,14 +628,23 @@ public final class DataModel {
return mAlarmModel.getAlarmVolumeButtonBehavior();
}
/**
public ThemeButtonBehavior getThemeButtonBehavior() {
enforceMainLooper();
return mThemeModel.getThemeButtonBehavior();
}
//
// Alarms
//
/**
* @return the behavior to execute when power buttons are pressed while firing an alarm
*/
public AlarmVolumeButtonBehavior getAlarmPowerButtonBehavior() {
enforceMainLooper();
return mAlarmModel.getAlarmPowerButtonBehavior();
}
/**
* @return the number of minutes an alarm may ring before it has timed out and becomes missed
*/
@ -751,10 +667,6 @@ public final class DataModel {
return mAlarmModel.getShakeAction();
}
//
// Stopwatch
//
/**
* @param stopwatchListener to be notified when stopwatch changes or laps are added
*/
@ -787,6 +699,10 @@ public final class DataModel {
return mStopwatchModel.setStopwatch(getStopwatch().start());
}
//
// Stopwatch
//
/**
* @return the stopwatch after being paused
*/
@ -844,11 +760,6 @@ public final class DataModel {
return mStopwatchModel.getCurrentLapTime(time);
}
//
// Time
// (Time settings/values are accessible from any Thread so no Thread-enforcement exists.)
//
/**
* @return the current time in milliseconds
*/
@ -878,7 +789,8 @@ public final class DataModel {
}
//
// Ringtones
// Time
// (Time settings/values are accessible from any Thread so no Thread-enforcement exists.)
//
/**
@ -909,7 +821,7 @@ public final class DataModel {
}
/**
* @param uri the uri of an audio file to use as a ringtone
* @param uri the uri of an audio file to use as a ringtone
* @param title the title of the audio content at the given {@code uri}
* @return the ringtone instance created for the audio file
*/
@ -918,6 +830,10 @@ public final class DataModel {
return mRingtoneModel.addCustomRingtone(uri, title);
}
//
// Ringtones
//
/**
* @param uri identifies the ringtone to remove
*/
@ -934,13 +850,9 @@ public final class DataModel {
return mRingtoneModel.getCustomRingtones();
}
//
// Widgets
//
/**
* @param widgetClass indicates the type of widget being counted
* @param count the number of widgets of the given type
* @param widgetClass indicates the type of widget being counted
* @param count the number of widgets of the given type
* @param eventCategoryId identifies the category of event to send
*/
public void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
@ -948,10 +860,6 @@ public final class DataModel {
mWidgetModel.updateWidgetCount(widgetClass, count, eventCategoryId);
}
//
// Settings
//
/**
* @param silentSettingsListener to be notified when alarm-silencing settings change
*/
@ -975,6 +883,10 @@ public final class DataModel {
return mSettingsModel.getGlobalIntentId();
}
//
// Widgets
//
/**
* Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
*/
@ -983,6 +895,10 @@ public final class DataModel {
mSettingsModel.updateGlobalIntentId();
}
//
// Settings
//
/**
* @return the style of clock to display in the clock application
*/
@ -1025,7 +941,7 @@ public final class DataModel {
/**
* @return {@code true} if the users wants to automatically show a clock for their home timezone
* when they have travelled outside of that timezone
* when they have travelled outside of that timezone
*/
public boolean getShowHomeClock() {
enforceMainLooper();
@ -1034,7 +950,7 @@ public final class DataModel {
/**
* @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY},
* {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
* {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
*/
public Weekdays.Order getWeekdayOrder() {
enforceMainLooper();
@ -1063,6 +979,129 @@ public final class DataModel {
return mSettingsModel.getTimeZones();
}
/**
* Indicates the display style of clocks.
*/
public enum ClockStyle {ANALOG, DIGITAL}
/**
* Indicates the preferred sort order of cities.
*/
public enum CitySort {NAME, UTC_OFFSET}
/**
* Indicates the preferred behavior of hardware volume buttons when firing alarms.
*/
public enum AlarmVolumeButtonBehavior {NOTHING, SNOOZE, DISMISS}
public enum ThemeButtonBehavior {SYSTEM, DARK, LIGHT}
/**
* Indicates the reason alarms may not fire or may fire silently.
*/
public enum SilentSetting {
DO_NOT_DISTURB(R.string.alarms_blocked_by_dnd, 0, Predicate.FALSE, null),
MUTED_VOLUME(R.string.alarm_volume_muted,
R.string.unmute_alarm_volume,
Predicate.TRUE,
new UnmuteAlarmVolumeListener()),
SILENT_RINGTONE(R.string.silent_default_alarm_ringtone,
R.string.change_setting_action,
new ChangeSoundActionPredicate(),
new ChangeSoundSettingsListener()),
BLOCKED_NOTIFICATIONS(R.string.app_notifications_blocked,
R.string.change_setting_action,
Predicate.TRUE,
new ChangeAppNotificationSettingsListener());
private final @StringRes
int mLabelResId;
private final @StringRes
int mActionResId;
private final Predicate<Context> mActionEnabled;
private final View.OnClickListener mActionListener;
SilentSetting(int labelResId, int actionResId, Predicate<Context> actionEnabled,
View.OnClickListener actionListener) {
mLabelResId = labelResId;
mActionResId = actionResId;
mActionEnabled = actionEnabled;
mActionListener = actionListener;
}
public @StringRes
int getLabelResId() {
return mLabelResId;
}
public @StringRes
int getActionResId() {
return mActionResId;
}
public View.OnClickListener getActionListener() {
return mActionListener;
}
public boolean isActionEnabled(Context context) {
return mLabelResId != 0 && mActionEnabled.apply(context);
}
private static class UnmuteAlarmVolumeListener implements View.OnClickListener {
@Override
public void onClick(View v) {
// Set the alarm volume to 11/16th of max and show the slider UI.
// 11/16th of max is the initial volume of the alarm stream on a fresh install.
final Context context = v.getContext();
final AudioManager am = (AudioManager) context.getSystemService(AUDIO_SERVICE);
final int index = Math.round(am.getStreamMaxVolume(STREAM_ALARM) * 11f / 16f);
am.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI);
}
}
private static class ChangeSoundSettingsListener implements View.OnClickListener {
@Override
public void onClick(View v) {
final Context context = v.getContext();
context.startActivity(new Intent(ACTION_SOUND_SETTINGS)
.addFlags(FLAG_ACTIVITY_NEW_TASK));
}
}
private static class ChangeSoundActionPredicate implements Predicate<Context> {
@Override
public boolean apply(Context context) {
final Intent intent = new Intent(ACTION_SOUND_SETTINGS);
return intent.resolveActivity(context.getPackageManager()) != null;
}
}
private static class ChangeAppNotificationSettingsListener implements View.OnClickListener {
@Override
public void onClick(View v) {
final Context context = v.getContext();
if (Utils.isLOrLater()) {
try {
// Attempt to open the notification settings for this app.
context.startActivity(
new Intent("android.settings.APP_NOTIFICATION_SETTINGS")
.putExtra("app_package", context.getPackageName())
.putExtra("app_uid", context.getApplicationInfo().uid)
.addFlags(FLAG_ACTIVITY_NEW_TASK));
return;
} catch (Exception ignored) {
// best attempt only; recovery code below
}
}
// Fall back to opening the app settings page.
context.startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", context.getPackageName(), null))
.addFlags(FLAG_ACTIVITY_NEW_TASK));
}
}
}
/**
* Used to execute a delegate runnable and track its completion.
*/

View file

@ -21,13 +21,19 @@ package com.best.deskclock.data;
*/
public final class Lap {
/** The 1-based position of the lap. */
/**
* The 1-based position of the lap.
*/
private final int mLapNumber;
/** Elapsed time in ms since the lap was last started. */
/**
* Elapsed time in ms since the lap was last started.
*/
private final long mLapTime;
/** Elapsed time in ms accumulated for all laps up to and including this one. */
/**
* Elapsed time in ms accumulated for all laps up to and including this one.
*/
private final long mAccumulatedTime;
Lap(int lapNumber, long lapTime, long accumulatedTime) {
@ -36,7 +42,15 @@ public final class Lap {
mAccumulatedTime = accumulatedTime;
}
public int getLapNumber() { return mLapNumber; }
public long getLapTime() { return mLapTime; }
public long getAccumulatedTime() { return mAccumulatedTime; }
public int getLapNumber() {
return mLapNumber;
}
public long getLapTime() {
return mLapTime;
}
public long getAccumulatedTime() {
return mAccumulatedTime;
}
}

View file

@ -23,13 +23,6 @@ final class NotificationModel {
private boolean mApplicationInForeground;
/**
* @param inForeground {@code true} to indicate the application is open in the foreground
*/
void setApplicationInForeground(boolean inForeground) {
mApplicationInForeground = inForeground;
}
/**
* @return {@code true} while the application is open in the foreground
*/
@ -37,6 +30,13 @@ final class NotificationModel {
return mApplicationInForeground;
}
/**
* @param inForeground {@code true} to indicate the application is open in the foreground
*/
void setApplicationInForeground(boolean inForeground) {
mApplicationInForeground = inForeground;
}
//
// Notification IDs
//

View file

@ -16,6 +16,9 @@
package com.best.deskclock.data;
import static android.media.AudioManager.STREAM_ALARM;
import static android.media.RingtoneManager.TITLE_COLUMN_INDEX;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@ -44,9 +47,6 @@ import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import static android.media.AudioManager.STREAM_ALARM;
import static android.media.RingtoneManager.TITLE_COLUMN_INDEX;
/**
* All ringtone data is accessed via this model.
*/
@ -56,14 +56,20 @@ final class RingtoneModel {
private final SharedPreferences mPrefs;
/** Maps ringtone uri to ringtone title; looking up a title from scratch is expensive. */
/**
* Maps ringtone uri to ringtone title; looking up a title from scratch is expensive.
*/
private final Map<Uri, String> mRingtoneTitles = new ArrayMap<>(16);
/** Clears data structures containing data that is locale-sensitive. */
/**
* Clears data structures containing data that is locale-sensitive.
*/
@SuppressWarnings("FieldCanBeLocal")
private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
/** A mutable copy of the custom ringtones. */
/**
* A mutable copy of the custom ringtones.
*/
private List<CustomRingtone> mCustomRingtones;
RingtoneModel(Context context, SharedPreferences prefs) {
@ -132,7 +138,7 @@ final class RingtoneModel {
permissions.add(uriPermission.getUri());
}
for (ListIterator<CustomRingtone> i = ringtones.listIterator(); i.hasNext();) {
for (ListIterator<CustomRingtone> i = ringtones.listIterator(); i.hasNext(); ) {
final CustomRingtone ringtone = i.next();
i.set(ringtone.setHasPermissions(permissions.contains(ringtone.getUri())));
}

View file

@ -16,18 +16,35 @@
package com.best.deskclock.data;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior.DISMISS;
import static com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior.NOTHING;
import static com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior.SNOOZE;
import static com.best.deskclock.data.DataModel.ThemeButtonBehavior.DARK;
import static com.best.deskclock.data.DataModel.ThemeButtonBehavior.LIGHT;
import static com.best.deskclock.data.DataModel.ThemeButtonBehavior.SYSTEM;
import static com.best.deskclock.data.Weekdays.Order.MON_TO_SUN;
import static com.best.deskclock.data.Weekdays.Order.SAT_TO_FRI;
import static com.best.deskclock.data.Weekdays.Order.SUN_TO_SAT;
import static java.util.Calendar.MONDAY;
import static java.util.Calendar.SATURDAY;
import static java.util.Calendar.SUNDAY;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.provider.Settings;
import androidx.annotation.NonNull;
import android.text.format.DateUtils;
import androidx.annotation.NonNull;
import com.best.deskclock.R;
import com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
import com.best.deskclock.data.DataModel.CitySort;
import com.best.deskclock.data.DataModel.ClockStyle;
import com.best.deskclock.data.DataModel.ThemeButtonBehavior;
import com.best.deskclock.settings.ScreensaverSettingsActivity;
import com.best.deskclock.settings.SettingsActivity;
@ -36,36 +53,33 @@ import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior.DISMISS;
import static com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior.NOTHING;
import static com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior.SNOOZE;
import static com.best.deskclock.data.Weekdays.Order.MON_TO_SUN;
import static com.best.deskclock.data.Weekdays.Order.SAT_TO_FRI;
import static com.best.deskclock.data.Weekdays.Order.SUN_TO_SAT;
import static java.util.Calendar.MONDAY;
import static java.util.Calendar.SATURDAY;
import static java.util.Calendar.SUNDAY;
/**
* This class encapsulates the storage of application preferences in {@link SharedPreferences}.
*/
final class SettingsDAO {
/** Key to a preference that stores the preferred sort order of world cities. */
/**
* Key to a preference that stores the preferred sort order of world cities.
*/
private static final String KEY_SORT_PREFERENCE = "sort_preference";
/** Key to a preference that stores the default ringtone for new alarms. */
/**
* Key to a preference that stores the default ringtone for new alarms.
*/
private static final String KEY_DEFAULT_ALARM_RINGTONE_URI = "default_alarm_ringtone_uri";
/** Key to a preference that stores the global broadcast id. */
/**
* Key to a preference that stores the global broadcast id.
*/
private static final String KEY_ALARM_GLOBAL_ID = "intent.extra.alarm.global.id";
/** Key to a preference that indicates whether restore (of backup and restore) has completed. */
/**
* Key to a preference that indicates whether restore (of backup and restore) has completed.
*/
private static final String KEY_RESTORE_BACKUP_FINISHED = "restore_finished";
private SettingsDAO() {}
private SettingsDAO() {
}
/**
* @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
@ -102,7 +116,7 @@ final class SettingsDAO {
/**
* @return {@code true} if a clock for the user's home timezone should be automatically
* displayed when it doesn't match the current timezone
* displayed when it doesn't match the current timezone
*/
static boolean getAutoShowHomeClock(SharedPreferences prefs) {
return prefs.getBoolean(SettingsActivity.KEY_AUTO_HOME_CLOCK, true);
@ -142,7 +156,7 @@ final class SettingsDAO {
* @return a value indicating whether analog or digital clocks are displayed in the app
*/
static boolean getDisplayClockSeconds(SharedPreferences prefs) {
return prefs.getBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, false);
return prefs.getBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, false);
}
/**
@ -180,7 +194,7 @@ final class SettingsDAO {
/**
* @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
* has yet been made
* has yet been made
*/
static Uri getTimerRingtoneUri(SharedPreferences prefs, Uri defaultUri) {
final String uriString = prefs.getString(SettingsActivity.KEY_TIMER_RINGTONE, null);
@ -210,7 +224,7 @@ final class SettingsDAO {
/**
* @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
* has yet been made
* has yet been made
*/
static Uri getDefaultAlarmRingtoneUri(SharedPreferences prefs) {
final String uriString = prefs.getString(KEY_DEFAULT_ALARM_RINGTONE_URI, null);
@ -226,7 +240,7 @@ final class SettingsDAO {
/**
* @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
* {@code 0} implies no crescendo should be applied
* {@code 0} implies no crescendo should be applied
*/
static long getAlarmCrescendoDuration(SharedPreferences prefs) {
final String crescendoSeconds = prefs.getString(SettingsActivity.KEY_ALARM_CRESCENDO, "0");
@ -235,7 +249,7 @@ final class SettingsDAO {
/**
* @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
* {@code 0} implies no crescendo should be applied
* {@code 0} implies no crescendo should be applied
*/
static long getTimerCrescendoDuration(SharedPreferences prefs) {
final String crescendoSeconds = prefs.getString(SettingsActivity.KEY_TIMER_CRESCENDO, "0");
@ -244,16 +258,19 @@ final class SettingsDAO {
/**
* @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY},
* {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
* {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
*/
static Weekdays.Order getWeekdayOrder(SharedPreferences prefs) {
final String defaultValue = String.valueOf(Calendar.getInstance().getFirstDayOfWeek());
final String value = prefs.getString(SettingsActivity.KEY_WEEK_START, defaultValue);
final int firstCalendarDay = Integer.parseInt(value);
switch (firstCalendarDay) {
case SATURDAY: return SAT_TO_FRI;
case SUNDAY: return SUN_TO_SAT;
case MONDAY: return MON_TO_SUN;
case SATURDAY:
return SAT_TO_FRI;
case SUNDAY:
return SUN_TO_SAT;
case MONDAY:
return MON_TO_SUN;
default:
throw new IllegalArgumentException("Unknown weekday: " + firstCalendarDay);
}
@ -284,14 +301,32 @@ final class SettingsDAO {
final String defaultValue = SettingsActivity.DEFAULT_VOLUME_BEHAVIOR;
final String value = prefs.getString(SettingsActivity.KEY_VOLUME_BUTTONS, defaultValue);
switch (value) {
case SettingsActivity.DEFAULT_VOLUME_BEHAVIOR: return NOTHING;
case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE: return SNOOZE;
case SettingsActivity.VOLUME_BEHAVIOR_DISMISS: return DISMISS;
case SettingsActivity.DEFAULT_VOLUME_BEHAVIOR:
return NOTHING;
case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE:
return SNOOZE;
case SettingsActivity.VOLUME_BEHAVIOR_DISMISS:
return DISMISS;
default:
throw new IllegalArgumentException("Unknown volume button behavior: " + value);
}
}
static ThemeButtonBehavior getThemeButtonBehavior(SharedPreferences prefs) {
final String defaultValue = SettingsActivity.SYSTEM_THEME_BEHAVIOR;
final String value = prefs.getString(SettingsActivity.KEY_THEME, defaultValue);
switch (value) {
case SettingsActivity.SYSTEM_THEME_BEHAVIOR:
return SYSTEM;
case SettingsActivity.THEME_BEHAVIOR_DARK:
return DARK;
case SettingsActivity.THEME_BEHAVIOR_LIGHT:
return LIGHT;
default:
throw new IllegalArgumentException("Unknown theme button behavior: " + value);
}
}
/**
* @return the behavior to execute when power buttons are pressed while firing an alarm
*/
@ -299,14 +334,17 @@ final class SettingsDAO {
final String defaultValue = SettingsActivity.DEFAULT_POWER_BEHAVIOR;
final String value = prefs.getString(SettingsActivity.KEY_POWER_BUTTONS, defaultValue);
switch (value) {
case SettingsActivity.DEFAULT_POWER_BEHAVIOR: return NOTHING;
case SettingsActivity.POWER_BEHAVIOR_SNOOZE: return SNOOZE;
case SettingsActivity.POWER_BEHAVIOR_DISMISS: return DISMISS;
case SettingsActivity.DEFAULT_POWER_BEHAVIOR:
return NOTHING;
case SettingsActivity.POWER_BEHAVIOR_SNOOZE:
return SNOOZE;
case SettingsActivity.POWER_BEHAVIOR_DISMISS:
return DISMISS;
default:
throw new IllegalArgumentException("Unknown power button behavior: " + value);
}
}
/**
* @return the number of minutes an alarm may ring before it has timed out and becomes missed
*/

View file

@ -25,6 +25,7 @@ import com.best.deskclock.Utils;
import com.best.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
import com.best.deskclock.data.DataModel.CitySort;
import com.best.deskclock.data.DataModel.ClockStyle;
import com.best.deskclock.data.DataModel.ThemeButtonBehavior;
import java.util.TimeZone;
@ -37,10 +38,14 @@ final class SettingsModel {
private final SharedPreferences mPrefs;
/** The model from which time data are fetched. */
/**
* The model from which time data are fetched.
*/
private final TimeModel mTimeModel;
/** The uri of the default ringtone to use for timers until the user explicitly chooses one. */
/**
* The uri of the default ringtone to use for timers until the user explicitly chooses one.
*/
private Uri mDefaultTimerRingtoneUri;
SettingsModel(Context context, SharedPreferences prefs, TimeModel timeModel) {
@ -113,22 +118,26 @@ final class SettingsModel {
return mDefaultTimerRingtoneUri;
}
void setTimerRingtoneUri(Uri uri) {
SettingsDAO.setTimerRingtoneUri(mPrefs, uri);
}
Uri getTimerRingtoneUri() {
return SettingsDAO.getTimerRingtoneUri(mPrefs, getDefaultTimerRingtoneUri());
}
void setTimerRingtoneUri(Uri uri) {
SettingsDAO.setTimerRingtoneUri(mPrefs, uri);
}
AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
return SettingsDAO.getAlarmVolumeButtonBehavior(mPrefs);
}
ThemeButtonBehavior getThemeButtonBehavior() {
return SettingsDAO.getThemeButtonBehavior(mPrefs);
}
AlarmVolumeButtonBehavior getAlarmPowerButtonBehavior() {
return SettingsDAO.getAlarmPowerButtonBehavior(mPrefs);
}
int getAlarmTimeout() {
return SettingsDAO.getAlarmTimeout(mPrefs);
}

View file

@ -16,7 +16,15 @@
package com.best.deskclock.data;
import android.annotation.TargetApi;
import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
import static android.content.Context.AUDIO_SERVICE;
import static android.content.Context.NOTIFICATION_SERVICE;
import static android.media.AudioManager.STREAM_ALARM;
import static android.media.RingtoneManager.TYPE_ALARM;
import static android.provider.Settings.System.CONTENT_URI;
import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@ -28,8 +36,8 @@ import android.media.AudioManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import androidx.core.app.NotificationManagerCompat;
import com.best.deskclock.Utils;
@ -38,15 +46,6 @@ import com.best.deskclock.data.DataModel.SilentSetting;
import java.util.ArrayList;
import java.util.List;
import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
import static android.content.Context.AUDIO_SERVICE;
import static android.content.Context.NOTIFICATION_SERVICE;
import static android.media.AudioManager.STREAM_ALARM;
import static android.media.RingtoneManager.TYPE_ALARM;
import static android.provider.Settings.System.CONTENT_URI;
import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI;
/**
* This model fetches and stores reasons that alarms may be suppressed or silenced by system
* settings on the device. This information is displayed passively to notify the user of this
@ -54,21 +53,31 @@ import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI;
*/
final class SilentSettingsModel {
/** The Uri to the settings entry that stores alarm stream volume. */
/**
* The Uri to the settings entry that stores alarm stream volume.
*/
private static final Uri VOLUME_URI = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker");
private final Context mContext;
/** Used to query the alarm volume and display the system control to change the alarm volume. */
/**
* Used to query the alarm volume and display the system control to change the alarm volume.
*/
private final AudioManager mAudioManager;
/** Used to query the do-not-disturb setting value, also called "interruption filter". */
/**
* Used to query the do-not-disturb setting value, also called "interruption filter".
*/
private final NotificationManager mNotificationManager;
/** Used to determine if the application is in the foreground. */
/**
* Used to determine if the application is in the foreground.
*/
private final NotificationModel mNotificationModel;
/** List of listeners to invoke upon silence state change. */
/**
* List of listeners to invoke upon silence state change.
*/
private final List<OnSilentSettingsListener> mListeners = new ArrayList<>(1);
/**
@ -77,7 +86,9 @@ final class SilentSettingsModel {
*/
private SilentSetting mSilentSetting;
/** The background task that checks the device system settings that influence alarm firing. */
/**
* The background task that checks the device system settings that influence alarm firing.
*/
private CheckSilenceSettingsTask mCheckSilenceSettingsTask;
SilentSettingsModel(Context context, NotificationModel notificationModel) {
@ -128,7 +139,7 @@ final class SilentSettingsModel {
/**
* @param silentSetting the latest notion of which setting is suppressing alarms; {@code null}
* if no settings are suppressing alarms
* if no settings are suppressing alarms
*/
private void setSilentState(SilentSetting silentSetting) {
if (mSilentSetting != silentSetting) {
@ -177,7 +188,6 @@ final class SilentSettingsModel {
}
}
@TargetApi(Build.VERSION_CODES.M)
private boolean isDoNotDisturbBlockingAlarms() {
if (!Utils.isMOrLater()) {
return false;

Some files were not shown because too many files have changed in this diff Show more