mirror of
https://github.com/BlackyHawky/Clock.git
synced 2025-07-01 22:51:12 +00:00
Updated To M3
This commit is contained in:
parent
56a8ac789b
commit
d90bc385d9
806 changed files with 25211 additions and 26467 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
41
.gitignore
vendored
41
.gitignore
vendored
|
@ -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
|
||||
|
|
40
Android.bp
40
Android.bp
|
@ -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,
|
||||
}
|
19
Android.mk
19
Android.mk
|
@ -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 $@
|
50
CleanSpec.mk
50
CleanSpec.mk
|
@ -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
201
LICENSE
|
@ -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.
|
190
NOTICE
190
NOTICE
|
@ -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
3
OWNERS
|
@ -1,3 +0,0 @@
|
|||
# This project has no significant updates recently.
|
||||
# Please update this list if you find better candidates.
|
||||
rtenneti@google.com
|
|
@ -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
67
app/build.gradle
Normal 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
21
app/proguard-rules.pro
vendored
Normal 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
|
|
@ -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" />
|
|
@ -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
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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) {
|
|
@ -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
|
|
@ -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);
|
|
@ -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);
|
|
@ -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() {}
|
||||
}
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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 {
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 = "";
|
|
@ -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()) {
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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.
|
||||
*/
|
|
@ -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() {
|
|
@ -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
|
|
@ -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.
|
||||
*/
|
|
@ -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);
|
|
@ -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) {
|
|
@ -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;
|
|
@ -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.
|
||||
*/
|
|
@ -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);
|
|
@ -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;
|
|
@ -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.
|
||||
*/
|
|
@ -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);
|
|
@ -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) {
|
|
@ -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);
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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).
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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()) {
|
|
@ -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();
|
||||
}
|
|
@ -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:
|
||||
*
|
|
@ -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) {
|
|
@ -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);
|
||||
}
|
|
@ -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) {
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//
|
|
@ -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())));
|
||||
}
|
|
@ -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
|
||||
*/
|
|
@ -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);
|
||||
}
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue