Compare commits
218 Commits
v1.8.9
...
v2.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 16b0682229 | |||
| a77798f293 | |||
| 08050ff616 | |||
| 12b080152b | |||
| df3d660e83 | |||
| 4908709296 | |||
| 5717ae1bf1 | |||
| 872c8d9d81 | |||
| 0b6110f0f9 | |||
| 6df5e9ebe9 | |||
| 2b9fd74a1d | |||
| 4a4b3c6aeb | |||
| 7979f74bea | |||
| b0336fb495 | |||
| 328fcd23f4 | |||
| 9c0951ae58 | |||
| 3f51561271 | |||
| 1787c0e74e | |||
| 49faacda1c | |||
| 339eeff1ff | |||
| 849212fd2f | |||
| 356b2b06e4 | |||
| b6eefbdb36 | |||
| 2a72601153 | |||
| 2bf7358207 | |||
| 8c6e2ef461 | |||
| ce0cbb6ee2 | |||
| 67ef3bb90c | |||
| b82af419f8 | |||
| 49ff9a7edf | |||
| 53ebdf4f14 | |||
| 4762b54701 | |||
| aa288ac406 | |||
| 2228dbf0f4 | |||
| 9ca1c8e459 | |||
| 61414d62f4 | |||
| f97fed3b9b | |||
| d45281d137 | |||
| 1bb6ad41b2 | |||
| 56c180183e | |||
| af8d983cca | |||
| 0a49232ebd | |||
| cff8e26428 | |||
| e892bccb32 | |||
| 68ccf37fd5 | |||
| 659c528744 | |||
| b1560dd694 | |||
| 0de86ac66c | |||
| 06e5d517cc | |||
| 35ca041bc2 | |||
| 576a334dc9 | |||
| 294aee5d12 | |||
| 0859cec853 | |||
| 23f2978a64 | |||
| a2400172e2 | |||
| 5376f4bff8 | |||
| 0497890cb0 | |||
| 2848c4e77b | |||
| 8fa3ba1b18 | |||
| b4f36dd258 | |||
| 008902d3b7 | |||
| 4764c07f3b | |||
| 8f0cfa8614 | |||
| 865e1969e6 | |||
|
|
bfddc42f5e | ||
| dc0b8deccf | |||
| b674d14b49 | |||
| d594d3b085 | |||
| bef85bf93a | |||
| 76eaefc95b | |||
|
|
2d488a67f2 | ||
|
|
d997b1378d | ||
| 720f98f9bd | |||
| ddea9e78a9 | |||
| c429cb41c0 | |||
| ae286cec14 | |||
| 31d631b155 | |||
| 20142d5f94 | |||
| ef186d55c6 | |||
| 8b847ae9fa | |||
|
|
a4ef657897 | ||
| f44556e281 | |||
| 8a895b2d20 | |||
| 61f32449dd | |||
|
|
07f8583c3d | ||
|
|
69f11c9d4e | ||
|
|
1ffc079042 | ||
| 5fa3f412c0 | |||
| b72cad5316 | |||
|
|
d59ab89426 | ||
| ea019321e6 | |||
| 6967def950 | |||
| 6d4cac427f | |||
| 152b2d5427 | |||
| 9d28fbe7b5 | |||
| c846dfc75a | |||
| ee7eb4ef51 | |||
|
|
57bfe3d801 | ||
| a5ee96f988 | |||
| 7b0eddeac5 | |||
| e8e52db9b1 | |||
| cddbf558e6 | |||
| 84523869e8 | |||
| 8067fd5313 | |||
|
|
d2dc756a34 | ||
| 2af1dbf3a6 | |||
| ebab6f08ee | |||
| 4a2b21855a | |||
| 42d5edec26 | |||
| f368e43158 | |||
| 09eb8c9f4d | |||
| 209e709163 | |||
| d20a2be7e6 | |||
| bd68f8fc5a | |||
|
|
1a05f7d85d | ||
|
|
d9ff429c28 | ||
| 3554895a5d | |||
| 16491c142a | |||
| 859fea5ff5 | |||
| 34c73e89db | |||
| 48e43869c7 | |||
| 963fb58309 | |||
| 3ef3be4d16 | |||
| bae0e3bcc1 | |||
| 3e99d821a5 | |||
|
|
acb5051eec | ||
|
|
b76882dd1d | ||
|
|
978946baab | ||
|
|
d202f14c14 | ||
|
|
17a85e517a | ||
|
|
c5bc5deff0 | ||
|
|
b7f04957a5 | ||
|
|
b0f5f96eee | ||
|
|
fd76a3c6fd | ||
| b31482881b | |||
|
|
87231d7fa4 | ||
|
|
4d18a1335c | ||
|
|
424a417a13 | ||
|
|
96d23bdf22 | ||
| a8e77b8df8 | |||
| 5413569ce3 | |||
| d80b85ac8c | |||
| 40bc35935f | |||
| a6060f468d | |||
| 6ec9d51a1e | |||
| de28a5e74e | |||
| 3ba503604b | |||
| 6d48b53861 | |||
| 0b7697d172 | |||
| b9850fa085 | |||
| ecb3978bdd | |||
| fc57a9db6c | |||
| 6c9c2a6c1a | |||
| 3e0529d515 | |||
| 7d8d89fbbd | |||
| c43f3c2fd7 | |||
| 403ed8b250 | |||
| 9ccb2b2737 | |||
| 424a282847 | |||
| 309b6cbcaf | |||
| 72ad14119a | |||
|
|
652ed50d09 | ||
| 6070a7af2e | |||
| 8fd8c2802b | |||
| 923b923745 | |||
| 59c8031372 | |||
| 0058089e7d | |||
| d5a840388c | |||
| 4b07d7d5b1 | |||
| 2fffc25128 | |||
| 4a4501276c | |||
|
|
c8e3735dd6 | ||
|
|
61267e40e7 | ||
|
|
c0b664e1e4 | ||
|
|
e57c319658 | ||
|
|
e54ba826b3 | ||
|
|
9b8784b4c4 | ||
|
|
51a7b7a7d4 | ||
|
|
d761b474cf | ||
|
|
51be585b9d | ||
|
|
76be5037fd | ||
| aee0da2c64 | |||
| f1610e6603 | |||
| a7a1766809 | |||
| 5f83314d56 | |||
| c784f40c55 | |||
| 6a172d135b | |||
| 13f4981066 | |||
|
|
849b91dde2 | ||
|
|
66b4c48d92 | ||
|
|
2e64da4cac | ||
| 323ddcc11a | |||
| 175000efd1 | |||
| 6f94fc48c1 | |||
|
|
18d1d0d9f7 | ||
|
|
47edea47ae | ||
|
|
1714cf8050 | ||
|
|
7366e9a47f | ||
|
|
e58589cfbd | ||
|
|
2999e0e5eb | ||
|
|
fa7bc27124 | ||
|
|
f5be9d3c67 | ||
|
|
2c46e8909a | ||
|
|
8b042f30dc | ||
|
|
46761926d2 | ||
|
|
88d6a8e513 | ||
|
|
557ae19297 | ||
|
|
9c10a56dda | ||
|
|
895b068321 | ||
|
|
fb98c5fe9a | ||
|
|
0ec604f21e | ||
|
|
bcd9dd1bb5 | ||
|
|
61bcd253f8 | ||
|
|
fb40dbdabc | ||
|
|
0990192cd6 | ||
|
|
1cf2d69534 | ||
|
|
0489dc39e0 | ||
|
|
88b9645be1 |
55
.github/workflows/main.yml
vendored
@@ -1,31 +1,50 @@
|
||||
name: Build & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macOS-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install Java Development Kit 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Build APK & Run small tests
|
||||
run: android/build.sh build
|
||||
- name: Run medium tests
|
||||
uses: ReactiveCircus/android-emulator-runner@v2.2.0
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
api-level: 29
|
||||
script: android/build.sh medium-tests
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
name: debug-apk
|
||||
path: android/build/*apk
|
||||
|
||||
- name: Upload build outputs
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Build
|
||||
name: build
|
||||
path: android/uhabits-android/build/outputs/
|
||||
|
||||
test:
|
||||
needs: build
|
||||
runs-on: macOS-latest
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [23, 24, 25, 26, 27, 28, 29]
|
||||
steps:
|
||||
- name: Check out source code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download previous build folder
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: android/uhabits-android/build/outputs/
|
||||
|
||||
- name: Run medium tests
|
||||
uses: ReactiveCircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
script: android/build.sh medium-tests
|
||||
|
||||
5
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
*.perspectivev3
|
||||
*.swp
|
||||
*~.nib
|
||||
*.hprof
|
||||
.DS_Store
|
||||
._.DS_Store
|
||||
.externalNativeBuild
|
||||
@@ -16,3 +17,7 @@ captures
|
||||
local.properties
|
||||
node_modules
|
||||
*xcuserdata*
|
||||
*.sketch
|
||||
/design
|
||||
/releases
|
||||
/screenshots
|
||||
|
||||
26
CHANGELOG.md
@@ -1,9 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
### 2.0.0 (TBD)
|
||||
|
||||
* **New Features:**
|
||||
* Track numerical habits (@iSoron, @namnl)
|
||||
* Skip days without breaking streak (@KristianTashkov)
|
||||
* Sort habits by status (@hiqua)
|
||||
* Sort habits in reverse order (@iSoron)
|
||||
* Add notes to habits (@recheej)
|
||||
* Improve readibility of charts (@chennemann)
|
||||
* Delay new day until 3am (@KristianTashkov)
|
||||
* Export backups daily (@iSoron)
|
||||
* **Bug fixes:**
|
||||
* Reset chart offset when switching scale (@alxmjo)
|
||||
* Don't show reminders from archived habits (@KristianTashkov)
|
||||
* Lapses on non-daily habits decrease the score too much (@iSoron)
|
||||
* Update widgets at midnight (@KristianTashkov)
|
||||
* **Refactoring:**
|
||||
* Convert files to Kotlin (@olegivo)
|
||||
|
||||
### 1.8.10 (Nov 26, 2020)
|
||||
|
||||
* Update translations
|
||||
|
||||
### 1.8.9 (Nov 18, 2020)
|
||||
|
||||
* Remove INTERNET permission
|
||||
* Manage exceptions for when activities don't exist to handle intents (#181)
|
||||
* Manage exceptions when activities don't exist to handle intents (#181)
|
||||
* MemoryHabitList: Inherit parent's order (#598)
|
||||
* Remove notification groups; revert to default system behavior
|
||||
* Remove SyncManager and Internet permission
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
<a href="https://github.com/iSoron/uhabits/actions?query=workflow%3A%22Build+%26+Test%22">
|
||||
<img src="https://github.com/iSoron/uhabits/workflows/Build%20&%20Test/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/iSoron/uhabits/releases">
|
||||
<img src="https://img.shields.io/github/v/release/iSoron/uhabits" />
|
||||
</a>
|
||||
|
||||
# Loop Habit Tracker
|
||||
|
||||
Loop is a mobile app that helps you create and maintain good habits,
|
||||
|
||||
1
android/.gitignore
vendored
@@ -23,6 +23,7 @@ docs/
|
||||
gen/
|
||||
local.properties
|
||||
crowdin.yaml
|
||||
crowdin.yml
|
||||
local
|
||||
tmp/
|
||||
secret/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion COMPILE_SDK_VERSION as Integer
|
||||
@@ -6,8 +7,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion MIN_SDK_VERSION as Integer
|
||||
targetSdkVersion TARGET_SDK_VERSION as Integer
|
||||
versionCode VERSION_CODE as Integer
|
||||
versionName "$VERSION_NAME"
|
||||
buildConfigField 'int', 'VERSION_CODE', "$VERSION_CODE"
|
||||
buildConfigField 'String', 'VERSION_NAME', "\"$VERSION_NAME\""
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -28,4 +29,5 @@ dependencies {
|
||||
implementation "org.apache.commons:commons-lang3:3.5"
|
||||
|
||||
annotationProcessor "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase;
|
||||
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.view.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
public class AndroidBugReporter
|
||||
{
|
||||
private final Context context;
|
||||
|
||||
@Inject
|
||||
public AndroidBugReporter(@NonNull @AppContext Context context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures and returns a bug report. The bug report contains some device
|
||||
* information and the logcat.
|
||||
*
|
||||
* @return a String containing the bug report.
|
||||
* @throws IOException when any I/O error occur.
|
||||
*/
|
||||
@NonNull
|
||||
public String getBugReport() throws IOException
|
||||
{
|
||||
String logcat = getLogcat();
|
||||
String deviceInfo = getDeviceInfo();
|
||||
|
||||
String log = "---------- BUG REPORT BEGINS ----------\n";
|
||||
log += deviceInfo + "\n" + logcat;
|
||||
log += "---------- BUG REPORT ENDS ------------\n";
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
public String getDeviceInfo()
|
||||
{
|
||||
if (context == null) return "null context\n";
|
||||
|
||||
WindowManager wm =
|
||||
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
|
||||
return
|
||||
String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME) +
|
||||
String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE) +
|
||||
String.format("OS Version: %s (%s)\n",
|
||||
System.getProperty("os.version"), Build.VERSION.INCREMENTAL) +
|
||||
String.format("OS API Level: %s\n", Build.VERSION.SDK) +
|
||||
String.format("Device: %s\n", Build.DEVICE) +
|
||||
String.format("Model (Product): %s (%s)\n", Build.MODEL,
|
||||
Build.PRODUCT) +
|
||||
String.format("Manufacturer: %s\n", Build.MANUFACTURER) +
|
||||
String.format("Other tags: %s\n", Build.TAGS) +
|
||||
String.format("Screen Width: %s\n",
|
||||
wm.getDefaultDisplay().getWidth()) +
|
||||
String.format("Screen Height: %s\n",
|
||||
wm.getDefaultDisplay().getHeight()) +
|
||||
String.format("External storage state: %s\n\n",
|
||||
Environment.getExternalStorageState());
|
||||
}
|
||||
|
||||
public String getLogcat() throws IOException
|
||||
{
|
||||
int maxLineCount = 250;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
String[] command = new String[]{ "logcat", "-d" };
|
||||
java.lang.Process process = Runtime.getRuntime().exec(command);
|
||||
|
||||
InputStreamReader in = new InputStreamReader(process.getInputStream());
|
||||
BufferedReader bufferedReader = new BufferedReader(in);
|
||||
|
||||
LinkedList<String> log = new LinkedList<>();
|
||||
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null)
|
||||
{
|
||||
log.addLast(line);
|
||||
if (log.size() > maxLineCount) log.removeFirst();
|
||||
}
|
||||
|
||||
for (String l : log)
|
||||
{
|
||||
builder.append(l);
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a bug report and saves it to a file in the SD card.
|
||||
* <p>
|
||||
* The contents of the file are generated by the method {@link
|
||||
* #getBugReport()}. The file is saved in the apps's external private
|
||||
* storage.
|
||||
*
|
||||
* @return the generated file.
|
||||
* @throws IOException when I/O errors occur.
|
||||
*/
|
||||
@NonNull
|
||||
public void dumpBugReportToFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
String date =
|
||||
new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US).format(
|
||||
new Date());
|
||||
|
||||
if (context == null) throw new IllegalStateException();
|
||||
|
||||
File dir = new AndroidDirFinder(context).getFilesDir("Logs");
|
||||
if (dir == null)
|
||||
throw new IOException("log dir should not be null");
|
||||
|
||||
File logFile =
|
||||
new File(String.format("%s/Log %s.txt", dir.getPath(), date));
|
||||
FileWriter output = new FileWriter(logFile);
|
||||
output.write(getBugReport());
|
||||
output.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.view.WindowManager
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
open class AndroidBugReporter @Inject constructor(@AppContext private val context: Context) {
|
||||
|
||||
/**
|
||||
* Captures and returns a bug report. The bug report contains some device
|
||||
* information and the logcat.
|
||||
*
|
||||
* @return a String containing the bug report.
|
||||
* @throws IOException when any I/O error occur.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getBugReport(): String {
|
||||
var log = "---------- BUG REPORT BEGINS ----------\n"
|
||||
log += "${getLogcat()}\n"
|
||||
log += "${getDeviceInfo()}\n"
|
||||
log += "---------- BUG REPORT ENDS ------------\n"
|
||||
return log
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getLogcat(): String {
|
||||
val maxLineCount = 250
|
||||
val builder = StringBuilder()
|
||||
val process = Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
|
||||
val inputReader = InputStreamReader(process.inputStream)
|
||||
val bufferedReader = BufferedReader(inputReader)
|
||||
val log = LinkedList<String>()
|
||||
var line: String?
|
||||
while (true) {
|
||||
line = bufferedReader.readLine()
|
||||
if (line == null) break;
|
||||
log.addLast(line)
|
||||
if (log.size > maxLineCount) log.removeFirst()
|
||||
}
|
||||
for (l in log) {
|
||||
builder.appendln(l)
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a bug report and saves it to a file in the SD card.
|
||||
*
|
||||
* The contents of the file are generated by the method [ ][.getBugReport]. The file is saved
|
||||
* in the apps's external private storage.
|
||||
*
|
||||
* @return the generated file.
|
||||
* @throws IOException when I/O errors occur.
|
||||
*/
|
||||
fun dumpBugReportToFile() {
|
||||
try {
|
||||
val date = SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US).format(Date())
|
||||
val dir = AndroidDirFinder(context).getFilesDir("Logs")
|
||||
?: throw IOException("log dir should not be null")
|
||||
val logFile = File(String.format("%s/Log %s.txt", dir.path, date))
|
||||
val output = FileWriter(logFile)
|
||||
output.write(getBugReport())
|
||||
output.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceInfo(): String {
|
||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
return buildString {
|
||||
appendln("App Version Name: ${BuildConfig.VERSION_NAME}")
|
||||
appendln("App Version Code: ${BuildConfig.VERSION_CODE}")
|
||||
appendln("OS Version: ${System.getProperty("os.version")} (${Build.VERSION.INCREMENTAL})")
|
||||
appendln("OS API Level: ${Build.VERSION.SDK_INT}")
|
||||
appendln("Device: ${Build.DEVICE}")
|
||||
appendln("Model (Product): ${Build.MODEL} (${Build.PRODUCT})")
|
||||
appendln("Manufacturer: ${Build.MANUFACTURER}")
|
||||
appendln("Other tags: ${Build.TAGS}")
|
||||
appendln("Screen Width: ${wm.defaultDisplay.width}")
|
||||
appendln("Screen Height: ${wm.defaultDisplay.height}")
|
||||
appendln("External storage state: ${Environment.getExternalStorageState()}")
|
||||
appendln()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase;
|
||||
|
||||
import android.content.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
public class AndroidDirFinder
|
||||
{
|
||||
@NonNull
|
||||
private Context context;
|
||||
|
||||
@Inject
|
||||
public AndroidDirFinder(@NonNull @AppContext Context context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public File getFilesDir(@Nullable String relativePath)
|
||||
{
|
||||
File externalFilesDirs[] =
|
||||
ContextCompat.getExternalFilesDirs(context, null);
|
||||
if (externalFilesDirs == null)
|
||||
{
|
||||
Log.e("BaseSystem",
|
||||
"getFilesDir: getExternalFilesDirs returned null");
|
||||
return null;
|
||||
}
|
||||
|
||||
return FileUtils.getDir(externalFilesDirs, relativePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.isoron.androidbase.utils.FileUtils
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class AndroidDirFinder @Inject constructor(@param:AppContext private val context: Context) {
|
||||
fun getFilesDir(relativePath: String): File? {
|
||||
return FileUtils.getDir(
|
||||
ContextCompat.getExternalFilesDirs(context, null),
|
||||
relativePath
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,11 @@
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase
|
||||
|
||||
package org.isoron.androidbase;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import javax.inject.*;
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AppContext
|
||||
{
|
||||
}
|
||||
@MustBeDocumented
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class AppContext
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
|
||||
public class BaseExceptionHandler implements Thread.UncaughtExceptionHandler
|
||||
{
|
||||
@Nullable
|
||||
private Thread.UncaughtExceptionHandler originalHandler;
|
||||
|
||||
@NonNull
|
||||
private BaseActivity activity;
|
||||
|
||||
public BaseExceptionHandler(@NonNull BaseActivity activity)
|
||||
{
|
||||
this.activity = activity;
|
||||
originalHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(@Nullable Thread thread,
|
||||
@Nullable Throwable ex)
|
||||
{
|
||||
if (ex == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
ex.printStackTrace();
|
||||
new AndroidBugReporter(activity).dumpBugReportToFile();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// if (ex.getCause() instanceof InconsistentDatabaseException)
|
||||
// {
|
||||
// HabitsApplication app = (HabitsApplication) activity.getApplication();
|
||||
// HabitList habits = app.getComponent().getHabitList();
|
||||
// habits.repair();
|
||||
// System.exit(0);
|
||||
// }
|
||||
|
||||
if (originalHandler != null)
|
||||
originalHandler.uncaughtException(thread, ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase
|
||||
|
||||
import org.isoron.androidbase.activities.BaseActivity
|
||||
|
||||
class BaseExceptionHandler(private val activity: BaseActivity) : Thread.UncaughtExceptionHandler {
|
||||
|
||||
private val originalHandler: Thread.UncaughtExceptionHandler? =
|
||||
Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
override fun uncaughtException(thread: Thread?, ex: Throwable?) {
|
||||
if (ex == null) return
|
||||
if (thread == null) return
|
||||
try {
|
||||
ex.printStackTrace()
|
||||
AndroidBugReporter(activity).dumpBugReportToFile()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
originalHandler?.uncaughtException(thread, ex)
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase;
|
||||
|
||||
import android.content.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.security.*;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.*;
|
||||
|
||||
import javax.inject.*;
|
||||
import javax.net.ssl.*;
|
||||
|
||||
public class SSLContextProvider
|
||||
{
|
||||
private Context context;
|
||||
|
||||
@Inject
|
||||
public SSLContextProvider(@NonNull @AppContext Context context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public SSLContext getCACertSSLContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
InputStream caInput = context.getAssets().open("cacert.pem");
|
||||
Certificate ca = cf.generateCertificate(caInput);
|
||||
|
||||
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
ks.load(null, null);
|
||||
ks.setCertificateEntry("ca", ca);
|
||||
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||||
TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(ks);
|
||||
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(null, tmf.getTrustManagers(), null);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase
|
||||
|
||||
import android.content.Context
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertificateFactory
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
|
||||
class SSLContextProvider @Inject constructor(@param:AppContext private val context: Context) {
|
||||
fun getCACertSSLContext(): SSLContext {
|
||||
try {
|
||||
val cf = CertificateFactory.getInstance("X.509")
|
||||
val ca = cf.generateCertificate(context.assets.open("cacert.pem"))
|
||||
val ks = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
|
||||
load(null, null)
|
||||
setCertificateEntry("ca", ca)
|
||||
}
|
||||
val alg = TrustManagerFactory.getDefaultAlgorithm()
|
||||
val tmf = TrustManagerFactory.getInstance(alg).apply {
|
||||
init(ks)
|
||||
}
|
||||
return SSLContext.getInstance("TLS").apply {
|
||||
init(null, tmf.trustManagers, null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,11 @@
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.activities
|
||||
|
||||
package org.isoron.androidbase.activities;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import javax.inject.*;
|
||||
import javax.inject.*
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ActivityContext
|
||||
{
|
||||
}
|
||||
@MustBeDocumented
|
||||
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class ActivityContext
|
||||
@@ -16,13 +16,12 @@
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.activities
|
||||
|
||||
package org.isoron.androidbase.activities;
|
||||
|
||||
import javax.inject.*;
|
||||
import javax.inject.*
|
||||
|
||||
/**
|
||||
* Scope used by objects that live as long as the activity is alive.
|
||||
*/
|
||||
@Scope
|
||||
public @interface ActivityScope { }
|
||||
annotation class ActivityScope
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.activities;
|
||||
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.*;
|
||||
import android.view.*;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
|
||||
import static android.R.anim.fade_in;
|
||||
import static android.R.anim.fade_out;
|
||||
|
||||
/**
|
||||
* Base class for all activities in the application.
|
||||
* <p>
|
||||
* This class delegates the responsibilities of an Android activity to other
|
||||
* classes. For example, callbacks related to menus are forwarded to a {@link
|
||||
* BaseMenu}, while callbacks related to activity results are forwarded to a
|
||||
* {@link BaseScreen}.
|
||||
* <p>
|
||||
* A BaseActivity also installs an {@link java.lang.Thread.UncaughtExceptionHandler}
|
||||
* to the main thread. By default, this handler is an instance of
|
||||
* BaseExceptionHandler, which logs the exception to the disk before the application
|
||||
* crashes. To the default handler, you should override the method
|
||||
* getExceptionHandler.
|
||||
*/
|
||||
abstract public class BaseActivity extends AppCompatActivity
|
||||
{
|
||||
@Nullable
|
||||
private BaseMenu baseMenu;
|
||||
|
||||
@Nullable
|
||||
private BaseScreen screen;
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(@Nullable Menu menu)
|
||||
{
|
||||
if (menu == null) return true;
|
||||
if (baseMenu == null) return true;
|
||||
baseMenu.onCreate(getMenuInflater(), menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@Nullable MenuItem item)
|
||||
{
|
||||
if (item == null) return false;
|
||||
if (baseMenu == null) return false;
|
||||
return baseMenu.onItemSelected(item);
|
||||
}
|
||||
|
||||
public void restartWithFade(Class<?> cls)
|
||||
{
|
||||
new Handler().postDelayed(() ->
|
||||
{
|
||||
finish();
|
||||
overridePendingTransition(fade_in, fade_out);
|
||||
startActivity(new Intent(this, cls));
|
||||
|
||||
}, 500); // HACK: Let the menu disappear first
|
||||
}
|
||||
|
||||
public void setBaseMenu(@Nullable BaseMenu baseMenu)
|
||||
{
|
||||
this.baseMenu = baseMenu;
|
||||
}
|
||||
|
||||
public void setScreen(@Nullable BaseScreen screen)
|
||||
{
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
public void showDialog(AppCompatDialogFragment dialog, String tag)
|
||||
{
|
||||
dialog.show(getSupportFragmentManager(), tag);
|
||||
}
|
||||
|
||||
public void showDialog(AppCompatDialog dialog)
|
||||
{
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int request, int result, Intent data)
|
||||
{
|
||||
if (screen == null) super.onActivityResult(request, result, data);
|
||||
else screen.onResult(request, result, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
Thread.setDefaultUncaughtExceptionHandler(getExceptionHandler());
|
||||
}
|
||||
|
||||
protected Thread.UncaughtExceptionHandler getExceptionHandler()
|
||||
{
|
||||
return new BaseExceptionHandler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume()
|
||||
{
|
||||
super.onResume();
|
||||
if(screen != null) screen.reattachDialogs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent)
|
||||
{
|
||||
try
|
||||
{
|
||||
super.startActivity(intent);
|
||||
}
|
||||
catch (ActivityNotFoundException e)
|
||||
{
|
||||
if (this.screen != null)
|
||||
this.screen.showMessage(R.string.activity_not_found);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.activities
|
||||
|
||||
import android.R.anim
|
||||
import android.content.*
|
||||
import android.os.*
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.*
|
||||
import org.isoron.androidbase.*
|
||||
|
||||
/**
|
||||
* Base class for all activities in the application.
|
||||
*
|
||||
* This class delegates the responsibilities of an Android activity to other classes. For example,
|
||||
* callbacks related to menus are forwarded to a []BaseMenu], while callbacks related to activity
|
||||
* results are forwarded to a [BaseScreen].
|
||||
*
|
||||
*
|
||||
* A BaseActivity also installs an [java.lang.Thread.UncaughtExceptionHandler] to the main thread.
|
||||
* By default, this handler is an instance of BaseExceptionHandler, which logs the exception to the
|
||||
* disk before the application crashes. To the default handler, you should override the method
|
||||
* getExceptionHandler.
|
||||
*/
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
private var baseMenu: BaseMenu? = null
|
||||
private var screen: BaseScreen? = null
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
if (menu != null) baseMenu?.onCreate(menuInflater, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
if (item == null) return false
|
||||
return baseMenu?.onItemSelected(item) ?: false
|
||||
}
|
||||
|
||||
fun restartWithFade(cls: Class<*>?) {
|
||||
Handler().postDelayed({
|
||||
finish()
|
||||
overridePendingTransition(anim.fade_in, anim.fade_out)
|
||||
startActivity(Intent(this, cls))
|
||||
}, 500) // HACK: Let the menu disappear first
|
||||
}
|
||||
|
||||
fun setBaseMenu(baseMenu: BaseMenu?) {
|
||||
this.baseMenu = baseMenu
|
||||
}
|
||||
|
||||
fun setScreen(screen: BaseScreen?) {
|
||||
this.screen = screen
|
||||
}
|
||||
|
||||
fun showDialog(dialog: AppCompatDialogFragment, tag: String?) {
|
||||
dialog.show(supportFragmentManager, tag)
|
||||
}
|
||||
|
||||
fun showDialog(dialog: AppCompatDialog) {
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
|
||||
val screen = screen
|
||||
if(screen == null) super.onActivityResult(request, result, data)
|
||||
else screen.onResult(request, result, data)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Thread.setDefaultUncaughtExceptionHandler(getExceptionHandler())
|
||||
}
|
||||
|
||||
private fun getExceptionHandler() = BaseExceptionHandler(this)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
screen?.reattachDialogs()
|
||||
}
|
||||
|
||||
override fun startActivity(intent: Intent?) {
|
||||
try {
|
||||
super.startActivity(intent)
|
||||
} catch(e: ActivityNotFoundException) {
|
||||
this.screen?.showMessage(R.string.activity_not_found)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,71 +16,50 @@
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.activities
|
||||
|
||||
package org.isoron.androidbase.activities;
|
||||
|
||||
import android.view.*;
|
||||
|
||||
import androidx.annotation.MenuRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.*
|
||||
import androidx.annotation.*
|
||||
|
||||
/**
|
||||
* Base class for all the menus in the application.
|
||||
* <p>
|
||||
*
|
||||
* This class receives from BaseActivity all callbacks related to menus, such as
|
||||
* menu creation and click events. It also handles some implementation details
|
||||
* of creating menus in Android, such as inflating the resources.
|
||||
*/
|
||||
public abstract class BaseMenu
|
||||
{
|
||||
@NonNull
|
||||
private final BaseActivity activity;
|
||||
|
||||
public BaseMenu(@NonNull BaseActivity activity)
|
||||
{
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public BaseActivity getActivity()
|
||||
{
|
||||
return activity;
|
||||
}
|
||||
abstract class BaseMenu(private val activity: BaseActivity) {
|
||||
|
||||
/**
|
||||
* Declare that the menu has changed, and should be recreated.
|
||||
*/
|
||||
public void invalidate()
|
||||
{
|
||||
activity.invalidateOptionsMenu();
|
||||
fun invalidate() {
|
||||
activity.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the menu is first displayed.
|
||||
* <p>
|
||||
*
|
||||
* The given menu is already inflated and ready to receive items. The
|
||||
* application should override this method and add items to the menu here.
|
||||
*
|
||||
* @param menu the menu that is being created.
|
||||
*/
|
||||
public void onCreate(@NonNull Menu menu)
|
||||
{
|
||||
}
|
||||
open fun onCreate(menu: Menu) {}
|
||||
|
||||
/**
|
||||
* Called when the menu is first displayed.
|
||||
* <p>
|
||||
*
|
||||
* This method should not be overridden. The application should override
|
||||
* the methods onCreate(Menu) and getMenuResourceId instead.
|
||||
*
|
||||
* @param inflater a menu inflater, for creating the menu
|
||||
* @param menu the menu that is being created.
|
||||
*/
|
||||
public void onCreate(@NonNull MenuInflater inflater, @NonNull Menu menu)
|
||||
{
|
||||
menu.clear();
|
||||
inflater.inflate(getMenuResourceId(), menu);
|
||||
onCreate(menu);
|
||||
fun onCreate(inflater: MenuInflater, menu: Menu) {
|
||||
menu.clear()
|
||||
inflater.inflate(getMenuResourceId(), menu)
|
||||
onCreate(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,10 +68,7 @@ public abstract class BaseMenu
|
||||
* @param item the item that was selected.
|
||||
* @return true if the event was consumed, or false otherwise
|
||||
*/
|
||||
public boolean onItemSelected(@NonNull MenuItem item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
open fun onItemSelected(item: MenuItem): Boolean = false
|
||||
|
||||
/**
|
||||
* Returns the id of the resource that should be used to inflate this menu.
|
||||
@@ -100,5 +76,6 @@ public abstract class BaseMenu
|
||||
* @return id of the menu resource.
|
||||
*/
|
||||
@MenuRes
|
||||
protected abstract int getMenuResourceId();
|
||||
}
|
||||
protected abstract fun getMenuResourceId(): Int
|
||||
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.activities;
|
||||
|
||||
import android.content.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
import org.isoron.androidbase.utils.*;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.os.Build.VERSION_CODES.LOLLIPOP;
|
||||
|
||||
/**
|
||||
* Base class for all root views in the application.
|
||||
* <p>
|
||||
* A root view is an Android view that is directly attached to an activity. This
|
||||
* view usually includes a toolbar and a progress bar. This abstract class hides
|
||||
* some of the complexity of setting these things up, for every version of
|
||||
* Android.
|
||||
*/
|
||||
public abstract class BaseRootView extends FrameLayout
|
||||
{
|
||||
@NonNull
|
||||
private final Context context;
|
||||
|
||||
protected boolean shouldDisplayHomeAsUp = false;
|
||||
|
||||
@Nullable
|
||||
private BaseScreen screen;
|
||||
|
||||
public BaseRootView(@NonNull Context context)
|
||||
{
|
||||
super(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public boolean getDisplayHomeAsUp()
|
||||
{
|
||||
return shouldDisplayHomeAsUp;
|
||||
}
|
||||
|
||||
public void setDisplayHomeAsUp(boolean b)
|
||||
{
|
||||
shouldDisplayHomeAsUp = b;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Toolbar getToolbar()
|
||||
{
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
if (toolbar == null) throw new RuntimeException(
|
||||
"Your BaseRootView should have a " +
|
||||
"toolbar with id R.id.toolbar");
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
public int getToolbarColor()
|
||||
{
|
||||
StyledResources res = new StyledResources(context);
|
||||
return res.getColor(R.attr.colorPrimary);
|
||||
}
|
||||
|
||||
protected void initToolbar()
|
||||
{
|
||||
if (SDK_INT >= LOLLIPOP)
|
||||
{
|
||||
getToolbar().setElevation(InterfaceUtils.dpToPixels(context, 2));
|
||||
|
||||
View view = findViewById(R.id.toolbarShadow);
|
||||
if (view != null) view.setVisibility(GONE);
|
||||
|
||||
view = findViewById(R.id.headerShadow);
|
||||
if (view != null) view.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void onAttachedToScreen(BaseScreen screen)
|
||||
{
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BaseScreen getScreen()
|
||||
{
|
||||
return screen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import org.isoron.androidbase.R
|
||||
import org.isoron.androidbase.utils.InterfaceUtils.dpToPixels
|
||||
import org.isoron.androidbase.utils.StyledResources
|
||||
|
||||
/**
|
||||
* Base class for all root views in the application.
|
||||
*
|
||||
*
|
||||
* A root view is an Android view that is directly attached to an activity. This
|
||||
* view usually includes a toolbar and a progress bar. This abstract class hides
|
||||
* some of the complexity of setting these things up, for every version of
|
||||
* Android.
|
||||
*/
|
||||
abstract class BaseRootView(context: Context) : FrameLayout(context) {
|
||||
var displayHomeAsUp = false
|
||||
var screen: BaseScreen? = null
|
||||
private set
|
||||
|
||||
open fun getToolbar(): Toolbar {
|
||||
return findViewById(R.id.toolbar)
|
||||
?: throw RuntimeException("Your BaseRootView should have a toolbar with id R.id.toolbar")
|
||||
}
|
||||
|
||||
open fun getToolbarColor(): Int = StyledResources(context).getColor(R.attr.colorPrimary)
|
||||
|
||||
protected open fun initToolbar() {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
||||
getToolbar().elevation = dpToPixels(context, 2f)
|
||||
findViewById<View>(R.id.toolbarShadow)?.visibility = View.GONE
|
||||
findViewById<View>(R.id.headerShadow)?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun onAttachedToScreen(screen: BaseScreen?) {
|
||||
this.screen = screen
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.activities;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.graphics.drawable.*;
|
||||
import android.net.*;
|
||||
import android.os.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.res.*;
|
||||
import androidx.appcompat.app.*;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
import org.isoron.androidbase.utils.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.os.Build.VERSION_CODES.LOLLIPOP;
|
||||
import static androidx.core.content.FileProvider.getUriForFile;
|
||||
|
||||
/**
|
||||
* Base class for all screens in the application.
|
||||
* <p>
|
||||
* Screens are responsible for deciding what root views and what menus should be
|
||||
* attached to the main window. They are also responsible for showing other
|
||||
* screens and for receiving their results.
|
||||
*/
|
||||
public class BaseScreen
|
||||
{
|
||||
protected BaseActivity activity;
|
||||
|
||||
@Nullable
|
||||
private BaseRootView rootView;
|
||||
|
||||
@Nullable
|
||||
private BaseSelectionMenu selectionMenu;
|
||||
|
||||
protected Snackbar snackbar;
|
||||
|
||||
public BaseScreen(@NonNull BaseActivity activity)
|
||||
{
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static int getDefaultActionBarColor(Context context)
|
||||
{
|
||||
if (SDK_INT < LOLLIPOP)
|
||||
{
|
||||
return ResourcesCompat.getColor(context.getResources(),
|
||||
R.color.grey_900, context.getTheme());
|
||||
}
|
||||
else
|
||||
{
|
||||
StyledResources res = new StyledResources(context);
|
||||
return res.getColor(R.attr.colorPrimary);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static void setupActionBarColor(@NonNull AppCompatActivity activity,
|
||||
int color)
|
||||
{
|
||||
|
||||
Toolbar toolbar = (Toolbar) activity.findViewById(R.id.toolbar);
|
||||
if (toolbar == null) return;
|
||||
|
||||
activity.setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = activity.getSupportActionBar();
|
||||
if (actionBar == null) return;
|
||||
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
ColorDrawable drawable = new ColorDrawable(color);
|
||||
actionBar.setBackgroundDrawable(drawable);
|
||||
|
||||
if (SDK_INT >= LOLLIPOP)
|
||||
{
|
||||
int darkerColor = ColorUtils.mixColors(color, Color.BLACK, 0.75f);
|
||||
activity.getWindow().setStatusBarColor(darkerColor);
|
||||
|
||||
toolbar.setElevation(InterfaceUtils.dpToPixels(activity, 2));
|
||||
|
||||
View view = activity.findViewById(R.id.toolbarShadow);
|
||||
if (view != null) view.setVisibility(View.GONE);
|
||||
|
||||
view = activity.findViewById(R.id.headerShadow);
|
||||
if (view != null) view.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the screen that its contents should be updated.
|
||||
*/
|
||||
public void invalidate()
|
||||
{
|
||||
if (rootView == null) return;
|
||||
rootView.invalidate();
|
||||
}
|
||||
|
||||
public void invalidateToolbar()
|
||||
{
|
||||
if (rootView == null) return;
|
||||
|
||||
activity.runOnUiThread(() ->
|
||||
{
|
||||
Toolbar toolbar = rootView.getToolbar();
|
||||
activity.setSupportActionBar(toolbar);
|
||||
ActionBar actionBar = activity.getSupportActionBar();
|
||||
if (actionBar == null) return;
|
||||
|
||||
actionBar.setDisplayHomeAsUpEnabled(rootView.getDisplayHomeAsUp());
|
||||
|
||||
int color = rootView.getToolbarColor();
|
||||
setActionBarColor(actionBar, color);
|
||||
setStatusBarColor(color);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when another Activity has finished, and has returned some result.
|
||||
*
|
||||
* @param requestCode the request code originally supplied to {@link
|
||||
* android.app.Activity#startActivityForResult(Intent,
|
||||
* int, Bundle)}.
|
||||
* @param resultCode the result code sent by the other activity.
|
||||
* @param data an Intent containing extra data sent by the other
|
||||
* activity.
|
||||
* @see {@link android.app.Activity#onActivityResult(int, int, Intent)}
|
||||
*/
|
||||
public void onResult(int requestCode, int resultCode, Intent data)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called after activity has been recreated, and the dialogs should be
|
||||
* reattached to their controllers.
|
||||
*/
|
||||
public void reattachDialogs()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the menu to be shown by this screen.
|
||||
* <p>
|
||||
* This menu will be visible if when there is no active selection operation.
|
||||
* If the provided menu is null, then no menu will be shown.
|
||||
*
|
||||
* @param menu the menu to be shown.
|
||||
*/
|
||||
public void setMenu(@Nullable BaseMenu menu)
|
||||
{
|
||||
activity.setBaseMenu(menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the root view for this screen.
|
||||
*
|
||||
* @param rootView the root view for this screen.
|
||||
*/
|
||||
public void setRootView(@Nullable BaseRootView rootView)
|
||||
{
|
||||
this.rootView = rootView;
|
||||
activity.setContentView(rootView);
|
||||
if (rootView == null) return;
|
||||
rootView.onAttachedToScreen(this);
|
||||
invalidateToolbar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the menu to be shown when a selection is active on the screen.
|
||||
*
|
||||
* @param menu the menu to be shown during a selection
|
||||
*/
|
||||
public void setSelectionMenu(@Nullable BaseSelectionMenu menu)
|
||||
{
|
||||
this.selectionMenu = menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message on the screen.
|
||||
*
|
||||
* @param stringId the string resource id for this message.
|
||||
*/
|
||||
public void showMessage(@StringRes Integer stringId)
|
||||
{
|
||||
if (stringId == null || rootView == null) return;
|
||||
if (snackbar == null)
|
||||
{
|
||||
snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT);
|
||||
int tvId = R.id.snackbar_text;
|
||||
TextView tv = (TextView) snackbar.getView().findViewById(tvId);
|
||||
tv.setTextColor(Color.WHITE);
|
||||
}
|
||||
else snackbar.setText(stringId);
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
public void showSendEmailScreen(@StringRes int toId,
|
||||
@StringRes int subjectId,
|
||||
String content)
|
||||
{
|
||||
String to = activity.getString(toId);
|
||||
String subject = activity.getString(subjectId);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.setType("message/rfc822");
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ to });
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, content);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
public void showSendFileScreen(@NonNull String archiveFilename)
|
||||
{
|
||||
File file = new File(archiveFilename);
|
||||
Uri fileUri = getUriForFile(activity, "org.isoron.uhabits", file);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.setType("application/zip");
|
||||
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
|
||||
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the screen to start a selection.
|
||||
* <p>
|
||||
* If a selection menu was provided, this menu will be shown instead of the
|
||||
* regular one.
|
||||
*/
|
||||
public void startSelection()
|
||||
{
|
||||
activity.startSupportActionMode(new ActionModeWrapper());
|
||||
}
|
||||
|
||||
private void setActionBarColor(@NonNull ActionBar actionBar, int color)
|
||||
{
|
||||
ColorDrawable drawable = new ColorDrawable(color);
|
||||
actionBar.setBackgroundDrawable(drawable);
|
||||
}
|
||||
|
||||
private void setStatusBarColor(int baseColor)
|
||||
{
|
||||
if (SDK_INT < LOLLIPOP) return;
|
||||
|
||||
int darkerColor = ColorUtils.mixColors(baseColor, Color.BLACK, 0.75f);
|
||||
activity.getWindow().setStatusBarColor(darkerColor);
|
||||
}
|
||||
|
||||
private class ActionModeWrapper implements ActionMode.Callback
|
||||
{
|
||||
@Override
|
||||
public boolean onActionItemClicked(@Nullable ActionMode mode,
|
||||
@Nullable MenuItem item)
|
||||
{
|
||||
if (item == null || selectionMenu == null) return false;
|
||||
return selectionMenu.onItemClicked(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(@Nullable ActionMode mode,
|
||||
@Nullable Menu menu)
|
||||
{
|
||||
if (selectionMenu == null) return false;
|
||||
if (mode == null || menu == null) return false;
|
||||
selectionMenu.onCreate(activity.getMenuInflater(), mode, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(@Nullable ActionMode mode)
|
||||
{
|
||||
if (selectionMenu == null) return;
|
||||
selectionMenu.onFinish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(@Nullable ActionMode mode,
|
||||
@Nullable Menu menu)
|
||||
{
|
||||
if (selectionMenu == null || menu == null) return false;
|
||||
return selectionMenu.onPrepare(menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.activities
|
||||
|
||||
import android.content.*
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.*
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import androidx.annotation.*
|
||||
import androidx.appcompat.app.*
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.*
|
||||
import com.google.android.material.snackbar.*
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.androidbase.utils.*
|
||||
import org.isoron.androidbase.utils.ColorUtils.mixColors
|
||||
import org.isoron.androidbase.utils.InterfaceUtils.dpToPixels
|
||||
import java.io.*
|
||||
|
||||
/**
|
||||
* Base class for all screens in the application.
|
||||
*
|
||||
* Screens are responsible for deciding what root views and what menus should be attached to the
|
||||
* main window. They are also responsible for showing other screens and for receiving their results.
|
||||
*/
|
||||
open class BaseScreen(@JvmField protected var activity: BaseActivity) {
|
||||
|
||||
private var rootView: BaseRootView? = null
|
||||
private var selectionMenu: BaseSelectionMenu? = null
|
||||
private var snackbar: Snackbar? = null
|
||||
|
||||
/**
|
||||
* Notifies the screen that its contents should be updated.
|
||||
*/
|
||||
fun invalidate() {
|
||||
rootView?.invalidate()
|
||||
}
|
||||
|
||||
fun invalidateToolbar() {
|
||||
rootView?.let { root ->
|
||||
activity.runOnUiThread {
|
||||
val toolbar = root.getToolbar()
|
||||
activity.setSupportActionBar(toolbar)
|
||||
activity.supportActionBar?.let { actionBar ->
|
||||
actionBar.setDisplayHomeAsUpEnabled(root.displayHomeAsUp)
|
||||
val color = root.getToolbarColor()
|
||||
setActionBarColor(actionBar, color)
|
||||
setStatusBarColor(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when another Activity has finished, and has returned some result.
|
||||
*
|
||||
* @param requestCode the request code originally supplied to startActivityForResult.
|
||||
* @param resultCode the result code sent by the other activity.
|
||||
* @param data an Intent containing extra data sent by the other
|
||||
* activity.
|
||||
* @see {@link android.app.Activity.onActivityResult
|
||||
*/
|
||||
open fun onResult(requestCode: Int, resultCode: Int, data: Intent?) {}
|
||||
|
||||
/**
|
||||
* Called after activity has been recreated, and the dialogs should be
|
||||
* reattached to their controllers.
|
||||
*/
|
||||
open fun reattachDialogs() {}
|
||||
|
||||
/**
|
||||
* Sets the menu to be shown by this screen.
|
||||
*
|
||||
*
|
||||
* This menu will be visible if when there is no active selection operation.
|
||||
* If the provided menu is null, then no menu will be shown.
|
||||
*
|
||||
* @param menu the menu to be shown.
|
||||
*/
|
||||
fun setMenu(menu: BaseMenu?) {
|
||||
activity.setBaseMenu(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the root view for this screen.
|
||||
*
|
||||
* @param rootView the root view for this screen.
|
||||
*/
|
||||
fun setRootView(rootView: BaseRootView?) {
|
||||
this.rootView = rootView
|
||||
activity.setContentView(rootView)
|
||||
rootView?.let {
|
||||
it.onAttachedToScreen(this)
|
||||
invalidateToolbar()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the menu to be shown when a selection is active on the screen.
|
||||
*
|
||||
* @param menu the menu to be shown during a selection
|
||||
*/
|
||||
fun setSelectionMenu(menu: BaseSelectionMenu?) {
|
||||
selectionMenu = menu
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message on the screen.
|
||||
*
|
||||
* @param stringId the string resource id for this message.
|
||||
*/
|
||||
fun showMessage(@StringRes stringId: Int?, rootView: View?) {
|
||||
var snackbar = this.snackbar
|
||||
if (stringId == null || rootView == null) return
|
||||
if (snackbar == null) {
|
||||
snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT)
|
||||
val tvId = R.id.snackbar_text
|
||||
val tv = snackbar.view.findViewById<TextView>(tvId)
|
||||
tv.setTextColor(Color.WHITE)
|
||||
this.snackbar = snackbar
|
||||
}
|
||||
snackbar.setText(stringId)
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
fun showMessage(@StringRes stringId: Int?) {
|
||||
showMessage(stringId, this.rootView)
|
||||
}
|
||||
|
||||
fun showSendEmailScreen(@StringRes toId: Int, @StringRes subjectId: Int, content: String?) {
|
||||
val to = activity.getString(toId)
|
||||
val subject = activity.getString(subjectId)
|
||||
activity.startActivity(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "message/rfc822"
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
|
||||
putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
putExtra(Intent.EXTRA_TEXT, content)
|
||||
})
|
||||
}
|
||||
|
||||
fun showSendFileScreen(archiveFilename: String) {
|
||||
val file = File(archiveFilename)
|
||||
val fileUri = FileProvider.getUriForFile(activity, "org.isoron.uhabits", file)
|
||||
activity.startActivity(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "application/zip"
|
||||
putExtra(Intent.EXTRA_STREAM, fileUri)
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the screen to start a selection.
|
||||
*
|
||||
* If a selection menu was provided, this menu will be shown instead of the regular one.
|
||||
*/
|
||||
fun startSelection() {
|
||||
activity.startSupportActionMode(ActionModeWrapper())
|
||||
}
|
||||
|
||||
private fun setActionBarColor(actionBar: ActionBar, color: Int) {
|
||||
val drawable = ColorDrawable(color)
|
||||
actionBar.setBackgroundDrawable(drawable)
|
||||
}
|
||||
|
||||
private fun setStatusBarColor(baseColor: Int) {
|
||||
val darkerColor = mixColors(baseColor, Color.BLACK, 0.75f)
|
||||
activity.window.statusBarColor = darkerColor
|
||||
}
|
||||
|
||||
private inner class ActionModeWrapper : ActionMode.Callback {
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
val selectionMenu = selectionMenu
|
||||
if (item == null || selectionMenu == null) return false
|
||||
return selectionMenu.onItemClicked(item)
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
if (mode == null || menu == null) return false
|
||||
val selectionMenu = selectionMenu ?: return false
|
||||
selectionMenu.onCreate(activity.menuInflater, mode, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
selectionMenu?.onFinish()
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
val selectionMenu = selectionMenu
|
||||
if (selectionMenu == null || menu == null) return false
|
||||
return selectionMenu.onPrepare(menu)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Deprecated("")
|
||||
fun getDefaultActionBarColor(context: Context) =
|
||||
StyledResources(context).getColor(R.attr.colorPrimary)
|
||||
|
||||
@JvmStatic
|
||||
@Deprecated("")
|
||||
fun setupActionBarColor(activity: AppCompatActivity, color: Int) {
|
||||
val toolbar = activity.findViewById<Toolbar>(R.id.toolbar) ?: return
|
||||
activity.setSupportActionBar(toolbar)
|
||||
val supportActionBar = activity.supportActionBar ?: return
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true)
|
||||
val drawable = ColorDrawable(color)
|
||||
supportActionBar.setBackgroundDrawable(drawable)
|
||||
val darkerColor = mixColors(color, Color.BLACK, 0.75f)
|
||||
activity.window.statusBarColor = darkerColor
|
||||
toolbar.elevation = dpToPixels(activity, 2f)
|
||||
activity.findViewById<View>(R.id.toolbarShadow)?.visibility = View.GONE
|
||||
activity.findViewById<View>(R.id.headerShadow)?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,50 +16,43 @@
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.activities
|
||||
|
||||
package org.isoron.androidbase.activities;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import android.view.*;
|
||||
import android.view.*
|
||||
import androidx.appcompat.view.ActionMode
|
||||
|
||||
/**
|
||||
* Base class for all the selection menus in the application.
|
||||
* <p>
|
||||
*
|
||||
* A selection menu is a menu that appears when the screen starts a selection
|
||||
* operation. It contains actions that modify the selected items, such as delete
|
||||
* or archive. Since it replaces the toolbar, it also has a title.
|
||||
* <p>
|
||||
*
|
||||
* This class hides many implementation details of creating such menus in
|
||||
* Android. The interface is supposed to look very similar to {@link BaseMenu},
|
||||
* Android. The interface is supposed to look very similar to [BaseMenu],
|
||||
* with a few additional methods, such as finishing the selection operation.
|
||||
* Internally, it uses an {@link ActionMode}.
|
||||
* Internally, it uses an [ActionMode].
|
||||
*/
|
||||
public abstract class BaseSelectionMenu
|
||||
{
|
||||
@Nullable
|
||||
private ActionMode actionMode;
|
||||
abstract class BaseSelectionMenu {
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Finishes the selection operation.
|
||||
*/
|
||||
public void finish()
|
||||
{
|
||||
if (actionMode != null) actionMode.finish();
|
||||
fun finish() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare that the menu has changed, and should be recreated.
|
||||
*/
|
||||
public void invalidate()
|
||||
{
|
||||
if (actionMode != null) actionMode.invalidate();
|
||||
fun invalidate() {
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the menu is first displayed.
|
||||
* <p>
|
||||
*
|
||||
* This method should not be overridden. The application should override
|
||||
* the methods onCreate(Menu) and getMenuResourceId instead.
|
||||
*
|
||||
@@ -67,22 +60,16 @@ public abstract class BaseSelectionMenu
|
||||
* @param mode the action mode associated with this menu.
|
||||
* @param menu the menu that is being created.
|
||||
*/
|
||||
public void onCreate(@NonNull MenuInflater inflater,
|
||||
@NonNull ActionMode mode,
|
||||
@NonNull Menu menu)
|
||||
{
|
||||
this.actionMode = mode;
|
||||
inflater.inflate(getResourceId(), menu);
|
||||
onCreate(menu);
|
||||
fun onCreate(inflater: MenuInflater, mode: ActionMode, menu: Menu) {
|
||||
actionMode = mode
|
||||
inflater.inflate(getResourceId(), menu)
|
||||
onCreate(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the selection operation is about to finish.
|
||||
*/
|
||||
public void onFinish()
|
||||
{
|
||||
|
||||
}
|
||||
open fun onFinish() {}
|
||||
|
||||
/**
|
||||
* Called whenever an item on the menu is selected.
|
||||
@@ -90,11 +77,7 @@ public abstract class BaseSelectionMenu
|
||||
* @param item the item that was selected.
|
||||
* @return true if the event was consumed, or false otherwise
|
||||
*/
|
||||
public boolean onItemClicked(@NonNull MenuItem item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
open fun onItemClicked(item: MenuItem): Boolean = false
|
||||
|
||||
/**
|
||||
* Called whenever the menu is invalidated.
|
||||
@@ -102,29 +85,23 @@ public abstract class BaseSelectionMenu
|
||||
* @param menu the menu to be refreshed
|
||||
* @return true if the menu has changes, false otherwise
|
||||
*/
|
||||
public boolean onPrepare(@NonNull Menu menu)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
open fun onPrepare(menu: Menu): Boolean = false
|
||||
|
||||
/**
|
||||
* Sets the title of the selection menu.
|
||||
*
|
||||
* @param title the new title.
|
||||
*/
|
||||
public void setTitle(String title)
|
||||
{
|
||||
if (actionMode != null) actionMode.setTitle(title);
|
||||
fun setTitle(title: String?) {
|
||||
actionMode?.title = title
|
||||
}
|
||||
|
||||
protected abstract int getResourceId();
|
||||
protected abstract fun getResourceId(): Int
|
||||
|
||||
/**
|
||||
* Called when the menu is first created.
|
||||
*
|
||||
* @param menu the menu being created
|
||||
*/
|
||||
protected void onCreate(@NonNull Menu menu)
|
||||
{
|
||||
}
|
||||
}
|
||||
protected fun onCreate(menu: Menu) {}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.utils;
|
||||
|
||||
import android.graphics.*;
|
||||
|
||||
public abstract class ColorUtils
|
||||
{
|
||||
public static int mixColors(int color1, int color2, float amount)
|
||||
{
|
||||
final byte ALPHA_CHANNEL = 24;
|
||||
final byte RED_CHANNEL = 16;
|
||||
final byte GREEN_CHANNEL = 8;
|
||||
final byte BLUE_CHANNEL = 0;
|
||||
|
||||
final float inverseAmount = 1.0f - amount;
|
||||
|
||||
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> ALPHA_CHANNEL & 0xff) *
|
||||
inverseAmount))) & 0xff;
|
||||
int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> RED_CHANNEL & 0xff) *
|
||||
inverseAmount))) & 0xff;
|
||||
int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) +
|
||||
((float) (color2 >> GREEN_CHANNEL & 0xff) *
|
||||
inverseAmount))) & 0xff;
|
||||
int b = ((int) (((float) (color1 & 0xff) * amount) +
|
||||
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
|
||||
|
||||
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL |
|
||||
b << BLUE_CHANNEL;
|
||||
}
|
||||
|
||||
public static int setAlpha(int color, float newAlpha)
|
||||
{
|
||||
int intAlpha = (int) (newAlpha * 255);
|
||||
return Color.argb(intAlpha, Color.red(color), Color.green(color),
|
||||
Color.blue(color));
|
||||
}
|
||||
|
||||
public static int setMinValue(int color, float newValue)
|
||||
{
|
||||
float hsv[] = new float[3];
|
||||
Color.colorToHSV(color, hsv);
|
||||
hsv[2] = Math.max(hsv[2], newValue);
|
||||
return Color.HSVToColor(hsv);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.utils
|
||||
|
||||
import android.graphics.Color
|
||||
import kotlin.math.max
|
||||
|
||||
object ColorUtils {
|
||||
private const val ALPHA_CHANNEL = 24
|
||||
private const val RED_CHANNEL = 16
|
||||
private const val GREEN_CHANNEL = 8
|
||||
private const val BLUE_CHANNEL = 0
|
||||
|
||||
@JvmStatic
|
||||
fun mixColors(color1: Int, color2: Int, amount: Float): Int {
|
||||
val a = mixColorChannel(color1, color2, amount, ALPHA_CHANNEL)
|
||||
val r = mixColorChannel(color1, color2, amount, RED_CHANNEL)
|
||||
val g = mixColorChannel(color1, color2, amount, GREEN_CHANNEL)
|
||||
val b = mixColorChannel(color1, color2, amount, BLUE_CHANNEL)
|
||||
return a or r or g or b
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setAlpha(color: Int, newAlpha: Float): Int {
|
||||
val intAlpha = (newAlpha * 255).toInt()
|
||||
return Color.argb(intAlpha, Color.red(color), Color.green(color), Color.blue(color))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setMinValue(color: Int, newValue: Float): Int {
|
||||
val hsv = FloatArray(3)
|
||||
Color.colorToHSV(color, hsv)
|
||||
hsv[2] = max(hsv[2], newValue)
|
||||
return Color.HSVToColor(hsv)
|
||||
}
|
||||
|
||||
private fun mixColorChannel(color1: Int, color2: Int, amount: Float, channel: Int): Int {
|
||||
val fl = (color1 shr channel and 0xff).toFloat() * amount
|
||||
val f2 = (color2 shr channel and 0xff).toFloat() * (1.0f - amount)
|
||||
return (fl + f2).toInt() and 0xff shl channel
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.utils;
|
||||
|
||||
import android.os.*;
|
||||
import android.util.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public abstract class FileUtils
|
||||
{
|
||||
public static void copy(File src, File dst) throws IOException
|
||||
{
|
||||
FileInputStream inStream = new FileInputStream(src);
|
||||
FileOutputStream outStream = new FileOutputStream(dst);
|
||||
copy(inStream, outStream);
|
||||
}
|
||||
|
||||
public static void copy(InputStream inStream, File dst) throws IOException
|
||||
{
|
||||
FileOutputStream outStream = new FileOutputStream(dst);
|
||||
copy(inStream, outStream);
|
||||
}
|
||||
|
||||
public static void copy(InputStream in, OutputStream out) throws IOException
|
||||
{
|
||||
int numBytes;
|
||||
byte[] buffer = new byte[1024];
|
||||
|
||||
while ((numBytes = in.read(buffer)) != -1)
|
||||
out.write(buffer, 0, numBytes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getDir(@NonNull File potentialParentDirs[],
|
||||
@Nullable String relativePath)
|
||||
{
|
||||
if (relativePath == null) relativePath = "";
|
||||
|
||||
File chosenDir = null;
|
||||
for (File dir : potentialParentDirs)
|
||||
{
|
||||
if (dir == null || !dir.canWrite()) continue;
|
||||
chosenDir = dir;
|
||||
break;
|
||||
}
|
||||
|
||||
if (chosenDir == null)
|
||||
{
|
||||
Log.e("FileUtils",
|
||||
"getDir: all potential parents are null or non-writable");
|
||||
return null;
|
||||
}
|
||||
|
||||
File dir = new File(
|
||||
String.format("%s/%s/", chosenDir.getAbsolutePath(), relativePath));
|
||||
if (!dir.exists() && !dir.mkdirs())
|
||||
{
|
||||
Log.e("FileUtils",
|
||||
"getDir: chosen dir does not exist and cannot be created");
|
||||
return null;
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getSDCardDir(@Nullable String relativePath)
|
||||
{
|
||||
File parents[] =
|
||||
new File[]{ Environment.getExternalStorageDirectory() };
|
||||
return getDir(parents, relativePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.utils
|
||||
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import java.io.*
|
||||
|
||||
|
||||
fun File.copyTo(dst: File) {
|
||||
val inStream = FileInputStream(this)
|
||||
val outStream = FileOutputStream(dst)
|
||||
inStream.copyTo(outStream)
|
||||
}
|
||||
|
||||
fun InputStream.copyTo(dst: File) {
|
||||
val outStream = FileOutputStream(dst)
|
||||
this.copyTo(outStream)
|
||||
}
|
||||
|
||||
fun InputStream.copyTo(out: OutputStream) {
|
||||
var numBytes: Int
|
||||
val buffer = ByteArray(1024)
|
||||
while (this.read(buffer).also { numBytes = it } != -1) {
|
||||
out.write(buffer, 0, numBytes)
|
||||
}
|
||||
}
|
||||
|
||||
object FileUtils {
|
||||
@JvmStatic
|
||||
fun getDir(potentialParentDirs: Array<File>, relativePath: String): File? {
|
||||
val chosenDir: File? = potentialParentDirs.firstOrNull { dir -> dir.canWrite() }
|
||||
if (chosenDir == null) {
|
||||
Log.e("FileUtils", "getDir: all potential parents are null or non-writable")
|
||||
return null
|
||||
}
|
||||
val dir = File("${chosenDir.absolutePath}/${relativePath}/")
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
Log.e("FileUtils", "getDir: chosen dir does not exist and cannot be created")
|
||||
return null
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getSDCardDir(relativePath: String): File? {
|
||||
val parents = arrayOf(Environment.getExternalStorageDirectory())
|
||||
return getDir(parents, relativePath)
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.utils;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.graphics.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
public abstract class InterfaceUtils
|
||||
{
|
||||
private static Typeface fontAwesome;
|
||||
|
||||
@Nullable
|
||||
private static Float fixedResolution = null;
|
||||
|
||||
public static void setFixedResolution(@NonNull Float f)
|
||||
{
|
||||
fixedResolution = f;
|
||||
}
|
||||
|
||||
public static Typeface getFontAwesome(Context context)
|
||||
{
|
||||
if(fontAwesome == null) fontAwesome =
|
||||
Typeface.createFromAsset(context.getAssets(),
|
||||
"fontawesome-webfont.ttf");
|
||||
|
||||
return fontAwesome;
|
||||
}
|
||||
|
||||
public static float dpToPixels(Context context, float dp)
|
||||
{
|
||||
if(fixedResolution != null) return dp * fixedResolution;
|
||||
|
||||
Resources resources = context.getResources();
|
||||
DisplayMetrics metrics = resources.getDisplayMetrics();
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
|
||||
}
|
||||
|
||||
public static float spToPixels(Context context, float sp)
|
||||
{
|
||||
if(fixedResolution != null) return sp * fixedResolution;
|
||||
|
||||
Resources resources = context.getResources();
|
||||
DisplayMetrics metrics = resources.getDisplayMetrics();
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
|
||||
}
|
||||
|
||||
public static float getDimension(Context context, int id)
|
||||
{
|
||||
float dim = context.getResources().getDimension(id);
|
||||
if (fixedResolution == null) return dim;
|
||||
else
|
||||
{
|
||||
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||
float actualDensity = dm.density;
|
||||
return dim / actualDensity * fixedResolution;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setupEditorAction(@NonNull ViewGroup parent,
|
||||
@NonNull TextView.OnEditorActionListener listener)
|
||||
{
|
||||
for (int i = 0; i < parent.getChildCount(); i++)
|
||||
{
|
||||
View child = parent.getChildAt(i);
|
||||
|
||||
if (child instanceof ViewGroup)
|
||||
setupEditorAction((ViewGroup) child, listener);
|
||||
|
||||
if (child instanceof TextView)
|
||||
((TextView) child).setOnEditorActionListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLayoutRtl(View view)
|
||||
{
|
||||
return ViewCompat.getLayoutDirection(view) ==
|
||||
ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.utils
|
||||
|
||||
import android.content.*
|
||||
import android.graphics.*
|
||||
import android.util.*
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import android.widget.TextView.*
|
||||
import androidx.core.view.*
|
||||
|
||||
object InterfaceUtils {
|
||||
private var fontAwesome: Typeface? = null
|
||||
private var fixedResolution: Float? = null
|
||||
|
||||
@JvmStatic
|
||||
fun setFixedResolution(f: Float) {
|
||||
fixedResolution = f
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getFontAwesome(context: Context): Typeface? {
|
||||
if (fontAwesome == null) {
|
||||
fontAwesome = Typeface.createFromAsset(context.assets, "fontawesome-webfont.ttf")
|
||||
}
|
||||
return fontAwesome
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun dpToPixels(context: Context, dp: Float): Float {
|
||||
if (fixedResolution != null) return dp * fixedResolution!!
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
||||
dp,
|
||||
context.resources.displayMetrics)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun spToPixels(context: Context, sp: Float): Float {
|
||||
if (fixedResolution != null) return sp * fixedResolution!!
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
|
||||
sp,
|
||||
context.resources.displayMetrics)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDimension(context: Context, id: Int): Float {
|
||||
val dim = context.resources.getDimension(id)
|
||||
if (fixedResolution != null) {
|
||||
val actualDensity = context.resources.displayMetrics.density
|
||||
return dim / actualDensity * fixedResolution!!
|
||||
}
|
||||
return dim
|
||||
}
|
||||
|
||||
fun setupEditorAction(parent: ViewGroup,
|
||||
listener: OnEditorActionListener) {
|
||||
for (i in 0 until parent.childCount) {
|
||||
val child = parent.getChildAt(i)
|
||||
if (child is ViewGroup) setupEditorAction(child, listener)
|
||||
if (child is TextView) child.setOnEditorActionListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun isLayoutRtl(view: View?): Boolean {
|
||||
return ViewCompat.getLayoutDirection(view!!) ==
|
||||
ViewCompat.LAYOUT_DIRECTION_RTL
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.androidbase.utils;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.graphics.drawable.*;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
|
||||
public class StyledResources
|
||||
{
|
||||
private static Integer fixedTheme;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public StyledResources(@NonNull Context context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static void setFixedTheme(Integer theme)
|
||||
{
|
||||
fixedTheme = theme;
|
||||
}
|
||||
|
||||
public boolean getBoolean(@AttrRes int attrId)
|
||||
{
|
||||
TypedArray ta = getTypedArray(attrId);
|
||||
boolean bool = ta.getBoolean(0, false);
|
||||
ta.recycle();
|
||||
|
||||
return bool;
|
||||
}
|
||||
|
||||
public int getDimension(@AttrRes int attrId)
|
||||
{
|
||||
TypedArray ta = getTypedArray(attrId);
|
||||
int dim = ta.getDimensionPixelSize(0, 0);
|
||||
ta.recycle();
|
||||
|
||||
return dim;
|
||||
}
|
||||
|
||||
public int getColor(@AttrRes int attrId)
|
||||
{
|
||||
TypedArray ta = getTypedArray(attrId);
|
||||
int color = ta.getColor(0, 0);
|
||||
ta.recycle();
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
public Drawable getDrawable(@AttrRes int attrId)
|
||||
{
|
||||
TypedArray ta = getTypedArray(attrId);
|
||||
Drawable drawable = ta.getDrawable(0);
|
||||
ta.recycle();
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public float getFloat(@AttrRes int attrId)
|
||||
{
|
||||
TypedArray ta = getTypedArray(attrId);
|
||||
float f = ta.getFloat(0, 0);
|
||||
ta.recycle();
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
public int[] getPalette()
|
||||
{
|
||||
int resourceId = getResource(R.attr.palette);
|
||||
if (resourceId < 0) throw new RuntimeException("resource not found");
|
||||
|
||||
return context.getResources().getIntArray(resourceId);
|
||||
}
|
||||
|
||||
public int getResource(@AttrRes int attrId)
|
||||
{
|
||||
TypedArray ta = getTypedArray(attrId);
|
||||
int resourceId = ta.getResourceId(0, -1);
|
||||
ta.recycle();
|
||||
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
private TypedArray getTypedArray(@AttrRes int attrId)
|
||||
{
|
||||
int[] attrs = new int[]{ attrId };
|
||||
|
||||
if (fixedTheme != null)
|
||||
return context.getTheme().obtainStyledAttributes(fixedTheme, attrs);
|
||||
|
||||
return context.obtainStyledAttributes(attrs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.androidbase.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.AttrRes
|
||||
import org.isoron.androidbase.R
|
||||
|
||||
class StyledResources(private val context: Context) {
|
||||
|
||||
fun getBoolean(@AttrRes attrId: Int): Boolean {
|
||||
val ta = getTypedArray(attrId)
|
||||
val bool = ta.getBoolean(0, false)
|
||||
ta.recycle()
|
||||
return bool
|
||||
}
|
||||
|
||||
fun getDimension(@AttrRes attrId: Int): Int {
|
||||
val ta = getTypedArray(attrId)
|
||||
val dim = ta.getDimensionPixelSize(0, 0)
|
||||
ta.recycle()
|
||||
return dim
|
||||
}
|
||||
|
||||
fun getColor(@AttrRes attrId: Int): Int {
|
||||
val ta = getTypedArray(attrId)
|
||||
val color = ta.getColor(0, 0)
|
||||
ta.recycle()
|
||||
return color
|
||||
}
|
||||
|
||||
fun getDrawable(@AttrRes attrId: Int): Drawable? {
|
||||
val ta = getTypedArray(attrId)
|
||||
val drawable = ta.getDrawable(0)
|
||||
ta.recycle()
|
||||
return drawable
|
||||
}
|
||||
|
||||
fun getFloat(@AttrRes attrId: Int): Float {
|
||||
val ta = getTypedArray(attrId)
|
||||
val f = ta.getFloat(0, 0f)
|
||||
ta.recycle()
|
||||
return f
|
||||
}
|
||||
|
||||
fun getPalette(): IntArray {
|
||||
val resourceId = getResource(R.attr.palette)
|
||||
if (resourceId < 0) throw RuntimeException("palette resource not found")
|
||||
return context.resources.getIntArray(resourceId)
|
||||
}
|
||||
|
||||
fun getResource(@AttrRes attrId: Int): Int {
|
||||
val ta = getTypedArray(attrId)
|
||||
val resourceId = ta.getResourceId(0, -1)
|
||||
ta.recycle()
|
||||
return resourceId
|
||||
}
|
||||
|
||||
private fun getTypedArray(@AttrRes attrId: Int): TypedArray {
|
||||
val attrs = intArrayOf(attrId)
|
||||
if (fixedTheme != null) {
|
||||
return context.theme.obtainStyledAttributes(fixedTheme!!, attrs)
|
||||
}
|
||||
return context.obtainStyledAttributes(attrs)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var fixedTheme: Int? = null
|
||||
|
||||
@JvmStatic
|
||||
fun setFixedTheme(theme: Int?) {
|
||||
fixedTheme = theme
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,8 @@ public class AmPmCirclesView extends View {
|
||||
private final Paint mPaint = new Paint();
|
||||
private int mSelectedAlpha;
|
||||
private int mUnselectedColor;
|
||||
private int mAmPmTextColor;
|
||||
private int mSelectedColor;
|
||||
protected int mAmPmTextColor = Color.WHITE;
|
||||
protected int mSelectedColor = Color.BLUE;
|
||||
private float mCircleRadiusMultiplier;
|
||||
private float mAmPmCircleRadiusMultiplier;
|
||||
private String mAmText;
|
||||
@@ -73,8 +73,8 @@ public class AmPmCirclesView extends View {
|
||||
|
||||
Resources res = context.getResources();
|
||||
mUnselectedColor = res.getColor(R.color.white);
|
||||
mSelectedColor = res.getColor(R.color.blue);
|
||||
mAmPmTextColor = res.getColor(R.color.ampm_text_color);
|
||||
//mSelectedColor = res.getColor(R.color.blue);
|
||||
//mAmPmTextColor = res.getColor(R.color.ampm_text_color);
|
||||
mSelectedAlpha = SELECTED_ALPHA;
|
||||
String typefaceFamily = res.getString(R.string.sans_serif);
|
||||
Typeface tf = Typeface.create(typefaceFamily, Typeface.NORMAL);
|
||||
@@ -105,8 +105,8 @@ public class AmPmCirclesView extends View {
|
||||
mSelectedAlpha = SELECTED_ALPHA_THEME_DARK;
|
||||
} else {
|
||||
mUnselectedColor = res.getColor(R.color.white);
|
||||
mSelectedColor = res.getColor(R.color.blue);
|
||||
mAmPmTextColor = res.getColor(R.color.ampm_text_color);
|
||||
//mSelectedColor = res.getColor(R.color.blue);
|
||||
//mAmPmTextColor = res.getColor(R.color.ampm_text_color);
|
||||
mSelectedAlpha = SELECTED_ALPHA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,14 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener {
|
||||
private AnimatorSet mTransition;
|
||||
private Handler mHandler = new Handler();
|
||||
|
||||
public void setColor(int selectedColor)
|
||||
{
|
||||
mHourRadialSelectorView.mPaint.setColor(selectedColor);
|
||||
mMinuteRadialSelectorView.mPaint.setColor(selectedColor);
|
||||
mAmPmCirclesView.mSelectedColor = selectedColor;
|
||||
mAmPmCirclesView.mAmPmTextColor = selectedColor;
|
||||
}
|
||||
|
||||
public interface OnValueSelectedListener {
|
||||
void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public class RadialSelectorView extends View {
|
||||
// Alpha level for the line.
|
||||
private static final int FULL_ALPHA = Utils.FULL_ALPHA;
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
protected final Paint mPaint = new Paint();
|
||||
|
||||
private boolean mIsInitialized;
|
||||
private boolean mDrawValuesReady;
|
||||
@@ -96,8 +96,6 @@ public class RadialSelectorView extends View {
|
||||
|
||||
Resources res = context.getResources();
|
||||
|
||||
int blue = res.getColor(R.color.blue);
|
||||
mPaint.setColor(blue);
|
||||
mPaint.setAntiAlias(true);
|
||||
mSelectionAlpha = SELECTED_ALPHA;
|
||||
|
||||
@@ -139,15 +137,11 @@ public class RadialSelectorView extends View {
|
||||
|
||||
/* package */ void setTheme(Context context, boolean themeDark) {
|
||||
Resources res = context.getResources();
|
||||
int color;
|
||||
if (themeDark) {
|
||||
color = res.getColor(R.color.red);
|
||||
mSelectionAlpha = SELECTED_ALPHA_THEME_DARK;
|
||||
} else {
|
||||
color = res.getColor(R.color.blue);
|
||||
mSelectionAlpha = SELECTED_ALPHA;
|
||||
}
|
||||
mPaint.setColor(color);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,9 @@ import android.app.*;
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.os.*;
|
||||
|
||||
import androidx.appcompat.app.*;
|
||||
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.view.View.*;
|
||||
@@ -39,7 +41,8 @@ import java.util.*;
|
||||
/**
|
||||
* Dialog to set a time.
|
||||
*/
|
||||
public class TimePickerDialog extends AppCompatDialogFragment implements OnValueSelectedListener{
|
||||
public class TimePickerDialog extends AppCompatDialogFragment implements OnValueSelectedListener
|
||||
{
|
||||
private static final String TAG = "TimePickerDialog";
|
||||
|
||||
private static final String KEY_HOUR_OF_DAY = "hour_of_day";
|
||||
@@ -49,6 +52,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
private static final String KEY_IN_KB_MODE = "in_kb_mode";
|
||||
private static final String KEY_TYPED_TIMES = "typed_times";
|
||||
private static final String KEY_DARK_THEME = "dark_theme";
|
||||
private static final String KEY_SELECTED_COLOR = "selected_color";
|
||||
|
||||
public static final int HOUR_INDEX = 0;
|
||||
public static final int MINUTE_INDEX = 1;
|
||||
@@ -108,37 +112,50 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
* The callback interface used to indicate the user is done filling in
|
||||
* the time (they clicked on the 'Set' button).
|
||||
*/
|
||||
public interface OnTimeSetListener {
|
||||
public interface OnTimeSetListener
|
||||
{
|
||||
|
||||
/**
|
||||
* @param view The view associated with this listener.
|
||||
* @param view The view associated with this listener.
|
||||
* @param hourOfDay The hour that was set.
|
||||
* @param minute The minute that was set.
|
||||
* @param minute The minute that was set.
|
||||
*/
|
||||
void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute);
|
||||
|
||||
default void onTimeCleared(RadialPickerLayout view) {}
|
||||
|
||||
default void onTimeCleared(RadialPickerLayout view)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public TimePickerDialog() {
|
||||
public TimePickerDialog()
|
||||
{
|
||||
// Empty constructor required for dialog fragment.
|
||||
}
|
||||
|
||||
@SuppressLint("Java")
|
||||
public TimePickerDialog(Context context, int theme, OnTimeSetListener callback,
|
||||
int hourOfDay, int minute, boolean is24HourMode) {
|
||||
int hourOfDay, int minute, boolean is24HourMode)
|
||||
{
|
||||
// Empty constructor required for dialog fragment.
|
||||
}
|
||||
|
||||
public static TimePickerDialog newInstance(OnTimeSetListener callback,
|
||||
int hourOfDay, int minute, boolean is24HourMode) {
|
||||
int hourOfDay,
|
||||
int minute,
|
||||
boolean is24HourMode,
|
||||
int color)
|
||||
{
|
||||
TimePickerDialog ret = new TimePickerDialog();
|
||||
ret.initialize(callback, hourOfDay, minute, is24HourMode);
|
||||
ret.initialize(callback, hourOfDay, minute, is24HourMode, color);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void initialize(OnTimeSetListener callback,
|
||||
int hourOfDay, int minute, boolean is24HourMode) {
|
||||
int hourOfDay,
|
||||
int minute,
|
||||
boolean is24HourMode,
|
||||
int color)
|
||||
{
|
||||
mCallback = callback;
|
||||
|
||||
mInitialHourOfDay = hourOfDay;
|
||||
@@ -146,40 +163,47 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
mIs24HourMode = is24HourMode;
|
||||
mInKbMode = false;
|
||||
mThemeDark = false;
|
||||
mSelectedColor = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a dark or light theme. NOTE: this will only take effect for the next onCreateView.
|
||||
*/
|
||||
public void setThemeDark(boolean dark) {
|
||||
public void setThemeDark(boolean dark)
|
||||
{
|
||||
mThemeDark = dark;
|
||||
}
|
||||
|
||||
public boolean isThemeDark() {
|
||||
public boolean isThemeDark()
|
||||
{
|
||||
return mThemeDark;
|
||||
}
|
||||
|
||||
public void setOnTimeSetListener(OnTimeSetListener callback) {
|
||||
public void setOnTimeSetListener(OnTimeSetListener callback)
|
||||
{
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
public void setStartTime(int hourOfDay, int minute) {
|
||||
public void setStartTime(int hourOfDay, int minute)
|
||||
{
|
||||
mInitialHourOfDay = hourOfDay;
|
||||
mInitialMinute = minute;
|
||||
mInKbMode = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY)
|
||||
&& savedInstanceState.containsKey(KEY_MINUTE)
|
||||
&& savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
|
||||
&& savedInstanceState.containsKey(KEY_MINUTE)
|
||||
&& savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
|
||||
mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY);
|
||||
mInitialMinute = savedInstanceState.getInt(KEY_MINUTE);
|
||||
mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW);
|
||||
mInKbMode = savedInstanceState.getBoolean(KEY_IN_KB_MODE);
|
||||
mThemeDark = savedInstanceState.getBoolean(KEY_DARK_THEME);
|
||||
mSelectedColor = savedInstanceState.getInt(KEY_SELECTED_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +215,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
Bundle savedInstanceState)
|
||||
{
|
||||
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
View view = inflater.inflate(R.layout.time_picker_dialog, null);
|
||||
@@ -203,8 +228,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
mSelectHours = res.getString(R.string.select_hours);
|
||||
mMinutePickerDescription = res.getString(R.string.minute_picker_description);
|
||||
mSelectMinutes = res.getString(R.string.select_minutes);
|
||||
mSelectedColor = res.getColor(mThemeDark? R.color.red : R.color.blue);
|
||||
mUnselectedColor = res.getColor(mThemeDark? R.color.white : R.color.numbers_text_color);
|
||||
//mSelectedColor = res.getColor(mThemeDark ? R.color.red : R.color.blue);
|
||||
mUnselectedColor = res.getColor(mThemeDark ? R.color.white : R.color.numbers_text_color);
|
||||
|
||||
mHourView = (TextView) view.findViewById(R.id.hours);
|
||||
mHourView.setOnKeyListener(keyboardListener);
|
||||
@@ -223,8 +248,9 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
mTimePicker = (RadialPickerLayout) view.findViewById(R.id.time_picker);
|
||||
mTimePicker.setOnValueSelectedListener(this);
|
||||
mTimePicker.setOnKeyListener(keyboardListener);
|
||||
mTimePicker.setColor(mSelectedColor);
|
||||
mTimePicker.initialize(getActivity(), mHapticFeedbackController, mInitialHourOfDay,
|
||||
mInitialMinute, mIs24HourMode);
|
||||
mInitialMinute, mIs24HourMode);
|
||||
|
||||
int currentItemShowing = HOUR_INDEX;
|
||||
if (savedInstanceState != null &&
|
||||
@@ -234,25 +260,31 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
setCurrentItemShowing(currentItemShowing, false, true, true);
|
||||
mTimePicker.invalidate();
|
||||
|
||||
mHourView.setOnClickListener(new OnClickListener() {
|
||||
mHourView.setOnClickListener(new OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
public void onClick(View v)
|
||||
{
|
||||
setCurrentItemShowing(HOUR_INDEX, true, false, true);
|
||||
tryVibrate();
|
||||
}
|
||||
});
|
||||
mMinuteView.setOnClickListener(new OnClickListener() {
|
||||
mMinuteView.setOnClickListener(new OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
public void onClick(View v)
|
||||
{
|
||||
setCurrentItemShowing(MINUTE_INDEX, true, false, true);
|
||||
tryVibrate();
|
||||
}
|
||||
});
|
||||
|
||||
mDoneButton = (TextView) view.findViewById(R.id.done_button);
|
||||
mDoneButton.setOnClickListener(new OnClickListener() {
|
||||
mDoneButton.setOnClickListener(new OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
public void onClick(View v)
|
||||
{
|
||||
if (mInKbMode && isTypedTimeFullyLegal()) {
|
||||
finishKbMode(false);
|
||||
} else {
|
||||
@@ -260,25 +292,25 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
if (mCallback != null) {
|
||||
mCallback.onTimeSet(mTimePicker,
|
||||
mTimePicker.getHours(), mTimePicker.getMinutes());
|
||||
mTimePicker.getHours(), mTimePicker.getMinutes());
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
mDoneButton.setOnKeyListener(keyboardListener);
|
||||
|
||||
|
||||
mClearButton = (TextView) view.findViewById(R.id.clear_button);
|
||||
mClearButton.setOnClickListener(new OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
if(mCallback != null) {
|
||||
mCallback.onTimeCleared(mTimePicker);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
if (mCallback != null) {
|
||||
mCallback.onTimeCleared(mTimePicker);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
mClearButton.setOnKeyListener(keyboardListener);
|
||||
|
||||
// Enable or disable the AM/PM view.
|
||||
@@ -293,15 +325,17 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
separatorView.setLayoutParams(paramsSeparator);
|
||||
} else {
|
||||
mAmPmTextView.setVisibility(View.VISIBLE);
|
||||
updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM);
|
||||
mAmPmHitspace.setOnClickListener(new OnClickListener() {
|
||||
updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM);
|
||||
mAmPmHitspace.setOnClickListener(new OnClickListener()
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
public void onClick(View v)
|
||||
{
|
||||
tryVibrate();
|
||||
int amOrPm = mTimePicker.getIsCurrentlyAmOrPm();
|
||||
if (amOrPm == AM) {
|
||||
amOrPm = PM;
|
||||
} else if (amOrPm == PM){
|
||||
} else if (amOrPm == PM) {
|
||||
amOrPm = AM;
|
||||
}
|
||||
updateAmPmDisplay(amOrPm);
|
||||
@@ -328,56 +362,61 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
mTypedTimes = new ArrayList<Integer>();
|
||||
}
|
||||
|
||||
// Set the theme at the end so that the initialize()s above don't counteract the theme.
|
||||
mTimePicker.setTheme(getActivity().getApplicationContext(), mThemeDark);
|
||||
// Prepare some palette to use.
|
||||
int white = res.getColor(R.color.white);
|
||||
int circleBackground = res.getColor(R.color.circle_background);
|
||||
int line = res.getColor(R.color.line_background);
|
||||
int timeDisplay = res.getColor(R.color.numbers_text_color);
|
||||
ColorStateList doneTextColor = res.getColorStateList(R.color.done_text_color);
|
||||
int doneBackground = R.drawable.done_background_color;
|
||||
|
||||
int darkGray = res.getColor(R.color.dark_gray);
|
||||
int lightGray = res.getColor(R.color.light_gray);
|
||||
int darkLine = res.getColor(R.color.line_dark);
|
||||
ColorStateList darkDoneTextColor = res.getColorStateList(R.color.done_text_color_dark);
|
||||
int darkDoneBackground = R.drawable.done_background_color_dark;
|
||||
// // Set the theme at the end so that the initialize()s above don't counteract the theme.
|
||||
// mTimePicker.setTheme(getActivity().getApplicationContext(), mThemeDark);
|
||||
// // Prepare some palette to use.
|
||||
// int white = res.getColor(R.color.white);
|
||||
// int circleBackground = res.getColor(R.color.circle_background);
|
||||
// int line = res.getColor(R.color.line_background);
|
||||
// int timeDisplay = res.getColor(R.color.numbers_text_color);
|
||||
// ColorStateList doneTextColor = res.getColorStateList(R.color.done_text_color);
|
||||
// int doneBackground = R.drawable.done_background_color;
|
||||
//
|
||||
// int darkGray = res.getColor(R.color.dark_gray);
|
||||
// int lightGray = res.getColor(R.color.light_gray);
|
||||
// int darkLine = res.getColor(R.color.line_dark);
|
||||
// ColorStateList darkDoneTextColor = res.getColorStateList(R.color.done_text_color_dark);
|
||||
// int darkDoneBackground = R.drawable.done_background_color_dark;
|
||||
|
||||
// Set the palette for each view based on the theme.
|
||||
view.findViewById(R.id.time_display_background).setBackgroundColor(mThemeDark? darkGray : white);
|
||||
view.findViewById(R.id.time_display).setBackgroundColor(mThemeDark? darkGray : white);
|
||||
((TextView) view.findViewById(R.id.separator)).setTextColor(mThemeDark? white : timeDisplay);
|
||||
((TextView) view.findViewById(R.id.ampm_label)).setTextColor(mThemeDark? white : timeDisplay);
|
||||
view.findViewById(R.id.line).setBackgroundColor(mThemeDark? darkLine : line);
|
||||
mDoneButton.setTextColor(mThemeDark? darkDoneTextColor : doneTextColor);
|
||||
mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground);
|
||||
mDoneButton.setBackgroundResource(mThemeDark? darkDoneBackground : doneBackground);
|
||||
// view.findViewById(R.id.time_display_background).setBackgroundColor(mThemeDark? darkGray : white);
|
||||
// view.findViewById(R.id.time_display).setBackgroundColor(mThemeDark? darkGray : white);
|
||||
// ((TextView) view.findViewById(R.id.separator)).setTextColor(mThemeDark? white : timeDisplay);
|
||||
// ((TextView) view.findViewById(R.id.ampm_label)).setTextColor(mThemeDark? white : timeDisplay);
|
||||
// view.findViewById(R.id.line).setBackgroundColor(mThemeDark? darkLine : line);
|
||||
// mDoneButton.setTextColor(mThemeDark? darkDoneTextColor : doneTextColor);
|
||||
// mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground);
|
||||
// mDoneButton.setBackgroundResource(mThemeDark? darkDoneBackground : doneBackground);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
public void onResume()
|
||||
{
|
||||
super.onResume();
|
||||
mHapticFeedbackController.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
public void onPause()
|
||||
{
|
||||
super.onPause();
|
||||
mHapticFeedbackController.stop();
|
||||
}
|
||||
|
||||
public void tryVibrate() {
|
||||
public void tryVibrate()
|
||||
{
|
||||
mHapticFeedbackController.tryVibrate();
|
||||
}
|
||||
|
||||
private void updateAmPmDisplay(int amOrPm) {
|
||||
private void updateAmPmDisplay(int amOrPm)
|
||||
{
|
||||
if (amOrPm == AM) {
|
||||
mAmPmTextView.setText(mAmText);
|
||||
Utils.tryAccessibilityAnnounce(mTimePicker, mAmText);
|
||||
mAmPmHitspace.setContentDescription(mAmText);
|
||||
} else if (amOrPm == PM){
|
||||
} else if (amOrPm == PM) {
|
||||
mAmPmTextView.setText(mPmText);
|
||||
Utils.tryAccessibilityAnnounce(mTimePicker, mPmText);
|
||||
mAmPmHitspace.setContentDescription(mPmText);
|
||||
@@ -387,7 +426,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
public void onSaveInstanceState(Bundle outState)
|
||||
{
|
||||
if (mTimePicker != null) {
|
||||
outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours());
|
||||
outState.putInt(KEY_MINUTE, mTimePicker.getMinutes());
|
||||
@@ -398,6 +438,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
outState.putIntegerArrayList(KEY_TYPED_TIMES, mTypedTimes);
|
||||
}
|
||||
outState.putBoolean(KEY_DARK_THEME, mThemeDark);
|
||||
outState.putInt(KEY_SELECTED_COLOR, mSelectedColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +446,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
* Called by the picker for updating the header display.
|
||||
*/
|
||||
@Override
|
||||
public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) {
|
||||
public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance)
|
||||
{
|
||||
if (pickerIndex == HOUR_INDEX) {
|
||||
setHour(newValue, false);
|
||||
String announcement = String.format("%d", newValue);
|
||||
@@ -417,7 +459,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
|
||||
Utils.tryAccessibilityAnnounce(mTimePicker, announcement);
|
||||
} else if (pickerIndex == MINUTE_INDEX){
|
||||
} else if (pickerIndex == MINUTE_INDEX) {
|
||||
setMinute(newValue);
|
||||
mTimePicker.setContentDescription(mMinutePickerDescription + ": " + newValue);
|
||||
} else if (pickerIndex == AMPM_INDEX) {
|
||||
@@ -430,7 +472,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
}
|
||||
|
||||
private void setHour(int value, boolean announce) {
|
||||
private void setHour(int value, boolean announce)
|
||||
{
|
||||
String format;
|
||||
if (mIs24HourMode) {
|
||||
format = "%02d";
|
||||
@@ -450,7 +493,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
}
|
||||
|
||||
private void setMinute(int value) {
|
||||
private void setMinute(int value)
|
||||
{
|
||||
if (value == 60) {
|
||||
value = 0;
|
||||
}
|
||||
@@ -462,7 +506,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
|
||||
// Show either Hours or Minutes.
|
||||
private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate,
|
||||
boolean announce) {
|
||||
boolean announce)
|
||||
{
|
||||
mTimePicker.setCurrentItemShowing(index, animateCircle);
|
||||
|
||||
TextView labelToAnimate;
|
||||
@@ -485,8 +530,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
labelToAnimate = mMinuteView;
|
||||
}
|
||||
|
||||
int hourColor = (index == HOUR_INDEX)? mSelectedColor : mUnselectedColor;
|
||||
int minuteColor = (index == MINUTE_INDEX)? mSelectedColor : mUnselectedColor;
|
||||
int hourColor = (index == HOUR_INDEX) ? mSelectedColor : mUnselectedColor;
|
||||
int minuteColor = (index == MINUTE_INDEX) ? mSelectedColor : mUnselectedColor;
|
||||
mHourView.setTextColor(hourColor);
|
||||
mMinuteView.setTextColor(minuteColor);
|
||||
|
||||
@@ -499,15 +544,17 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
|
||||
/**
|
||||
* For keyboard mode, processes key events.
|
||||
*
|
||||
* @param keyCode the pressed key.
|
||||
* @return true if the key was successfully processed, false otherwise.
|
||||
*/
|
||||
private boolean processKeyUp(int keyCode) {
|
||||
private boolean processKeyUp(int keyCode)
|
||||
{
|
||||
if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
dismiss();
|
||||
return true;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_TAB) {
|
||||
if(mInKbMode) {
|
||||
if (mInKbMode) {
|
||||
if (isTypedTimeFullyLegal()) {
|
||||
finishKbMode(true);
|
||||
}
|
||||
@@ -522,7 +569,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
if (mCallback != null) {
|
||||
mCallback.onTimeSet(mTimePicker,
|
||||
mTimePicker.getHours(), mTimePicker.getMinutes());
|
||||
mTimePicker.getHours(), mTimePicker.getMinutes());
|
||||
}
|
||||
dismiss();
|
||||
return true;
|
||||
@@ -539,7 +586,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
deletedKeyStr = String.format("%d", getValFromKeyCode(deleted));
|
||||
}
|
||||
Utils.tryAccessibilityAnnounce(mTimePicker,
|
||||
String.format(mDeletedKeyFormat, deletedKeyStr));
|
||||
String.format(mDeletedKeyFormat, deletedKeyStr));
|
||||
updateDisplay(true);
|
||||
}
|
||||
}
|
||||
@@ -549,7 +596,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
|| keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
|
||||
|| keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9
|
||||
|| (!mIs24HourMode &&
|
||||
(keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
|
||||
(keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
|
||||
if (!mInKbMode) {
|
||||
if (mTimePicker == null) {
|
||||
// Something's wrong, because time picker should definitely not be null.
|
||||
@@ -572,11 +619,13 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
/**
|
||||
* Try to start keyboard mode with the specified key, as long as the timepicker is not in the
|
||||
* middle of a touch-event.
|
||||
*
|
||||
* @param keyCode The key to use as the first press. Keyboard mode will not be started if the
|
||||
* key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting
|
||||
* key.
|
||||
* key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting
|
||||
* key.
|
||||
*/
|
||||
private void tryStartingKbMode(int keyCode) {
|
||||
private void tryStartingKbMode(int keyCode)
|
||||
{
|
||||
if (mTimePicker.trySettingInputEnabled(false) &&
|
||||
(keyCode == -1 || addKeyIfLegal(keyCode))) {
|
||||
mInKbMode = true;
|
||||
@@ -585,7 +634,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
}
|
||||
|
||||
private boolean addKeyIfLegal(int keyCode) {
|
||||
private boolean addKeyIfLegal(int keyCode)
|
||||
{
|
||||
// If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode,
|
||||
// we'll need to see if AM/PM have been typed.
|
||||
if ((mIs24HourMode && mTypedTimes.size() == 4) ||
|
||||
@@ -617,7 +667,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
* Traverse the tree to see if the keys that have been typed so far are legal as is,
|
||||
* or may become legal as more keys are typed (excluding backspace).
|
||||
*/
|
||||
private boolean isTypedTimeLegalSoFar() {
|
||||
private boolean isTypedTimeLegalSoFar()
|
||||
{
|
||||
Node node = mLegalTimesTree;
|
||||
for (int keyCode : mTypedTimes) {
|
||||
node = node.canReach(keyCode);
|
||||
@@ -631,7 +682,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
/**
|
||||
* Check if the time that has been typed so far is completely legal, as is.
|
||||
*/
|
||||
private boolean isTypedTimeFullyLegal() {
|
||||
private boolean isTypedTimeFullyLegal()
|
||||
{
|
||||
if (mIs24HourMode) {
|
||||
// For 24-hour mode, the time is legal if the hours and minutes are each legal. Note:
|
||||
// getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode.
|
||||
@@ -645,7 +697,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
}
|
||||
|
||||
private int deleteLastTypedKey() {
|
||||
private int deleteLastTypedKey()
|
||||
{
|
||||
int deleted = mTypedTimes.remove(mTypedTimes.size() - 1);
|
||||
if (!isTypedTimeFullyLegal()) {
|
||||
mDoneButton.setEnabled(false);
|
||||
@@ -655,9 +708,11 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
|
||||
/**
|
||||
* Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time.
|
||||
*
|
||||
* @param changeDisplays If true, update the displays with the relevant time.
|
||||
*/
|
||||
private void finishKbMode(boolean updateDisplays) {
|
||||
private void finishKbMode(boolean updateDisplays)
|
||||
{
|
||||
mInKbMode = false;
|
||||
if (!mTypedTimes.isEmpty()) {
|
||||
int values[] = getEnteredTime(null);
|
||||
@@ -677,29 +732,31 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
* Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is
|
||||
* empty, either show an empty display (filled with the placeholder text), or update from the
|
||||
* timepicker's values.
|
||||
*
|
||||
* @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text.
|
||||
* Otherwise, revert to the timepicker's values.
|
||||
* Otherwise, revert to the timepicker's values.
|
||||
*/
|
||||
private void updateDisplay(boolean allowEmptyDisplay) {
|
||||
private void updateDisplay(boolean allowEmptyDisplay)
|
||||
{
|
||||
if (!allowEmptyDisplay && mTypedTimes.isEmpty()) {
|
||||
int hour = mTimePicker.getHours();
|
||||
int minute = mTimePicker.getMinutes();
|
||||
setHour(hour, true);
|
||||
setMinute(minute);
|
||||
if (!mIs24HourMode) {
|
||||
updateAmPmDisplay(hour < 12? AM : PM);
|
||||
updateAmPmDisplay(hour < 12 ? AM : PM);
|
||||
}
|
||||
setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true, true, true);
|
||||
mDoneButton.setEnabled(true);
|
||||
} else {
|
||||
Boolean[] enteredZeros = {false, false};
|
||||
int[] values = getEnteredTime(enteredZeros);
|
||||
String hourFormat = enteredZeros[0]? "%02d" : "%2d";
|
||||
String minuteFormat = (enteredZeros[1])? "%02d" : "%2d";
|
||||
String hourStr = (values[0] == -1)? mDoublePlaceholderText :
|
||||
String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
|
||||
String minuteStr = (values[1] == -1)? mDoublePlaceholderText :
|
||||
String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
|
||||
String hourFormat = enteredZeros[0] ? "%02d" : "%2d";
|
||||
String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d";
|
||||
String hourStr = (values[0] == -1) ? mDoublePlaceholderText :
|
||||
String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
|
||||
String minuteStr = (values[1] == -1) ? mDoublePlaceholderText :
|
||||
String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
|
||||
mHourView.setText(hourStr);
|
||||
mHourSpaceView.setText(hourStr);
|
||||
mHourView.setTextColor(mUnselectedColor);
|
||||
@@ -712,7 +769,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
}
|
||||
|
||||
private static int getValFromKeyCode(int keyCode) {
|
||||
private static int getValFromKeyCode(int keyCode)
|
||||
{
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_0:
|
||||
return 0;
|
||||
@@ -741,20 +799,22 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
|
||||
/**
|
||||
* Get the currently-entered time, as integer values of the hours and minutes typed.
|
||||
*
|
||||
* @param enteredZeros A size-2 boolean array, which the caller should initialize, and which
|
||||
* may then be used for the caller to know whether zeros had been explicitly entered as either
|
||||
* hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's.
|
||||
* may then be used for the caller to know whether zeros had been explicitly entered as either
|
||||
* hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's.
|
||||
* @return A size-3 int array. The first value will be the hours, the second value will be the
|
||||
* minutes, and the third will be either TimePickerDialog.AM or TimePickerDialog.PM.
|
||||
*/
|
||||
private int[] getEnteredTime(Boolean[] enteredZeros) {
|
||||
private int[] getEnteredTime(Boolean[] enteredZeros)
|
||||
{
|
||||
int amOrPm = -1;
|
||||
int startIndex = 1;
|
||||
if (!mIs24HourMode && isTypedTimeFullyLegal()) {
|
||||
int keyCode = mTypedTimes.get(mTypedTimes.size() - 1);
|
||||
if (keyCode == getAmOrPmKeyCode(AM)) {
|
||||
amOrPm = AM;
|
||||
} else if (keyCode == getAmOrPmKeyCode(PM)){
|
||||
} else if (keyCode == getAmOrPmKeyCode(PM)) {
|
||||
amOrPm = PM;
|
||||
}
|
||||
startIndex = 2;
|
||||
@@ -765,15 +825,15 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i));
|
||||
if (i == startIndex) {
|
||||
minute = val;
|
||||
} else if (i == startIndex+1) {
|
||||
minute += 10*val;
|
||||
} else if (i == startIndex + 1) {
|
||||
minute += 10 * val;
|
||||
if (enteredZeros != null && val == 0) {
|
||||
enteredZeros[1] = true;
|
||||
}
|
||||
} else if (i == startIndex+2) {
|
||||
} else if (i == startIndex + 2) {
|
||||
hour = val;
|
||||
} else if (i == startIndex+3) {
|
||||
hour += 10*val;
|
||||
} else if (i == startIndex + 3) {
|
||||
hour += 10 * val;
|
||||
if (enteredZeros != null && val == 0) {
|
||||
enteredZeros[0] = true;
|
||||
}
|
||||
@@ -787,7 +847,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
/**
|
||||
* Get the keycode value for AM and PM in the current language.
|
||||
*/
|
||||
private int getAmOrPmKeyCode(int amOrPm) {
|
||||
private int getAmOrPmKeyCode(int amOrPm)
|
||||
{
|
||||
// Cache the codes.
|
||||
if (mAmKeyCode == -1 || mPmKeyCode == -1) {
|
||||
// Find the first character in the AM/PM text that is unique.
|
||||
@@ -822,7 +883,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
/**
|
||||
* Create a tree for deciding what keys can legally be typed.
|
||||
*/
|
||||
private void generateLegalTimesTree() {
|
||||
private void generateLegalTimesTree()
|
||||
{
|
||||
// Create a quick cache of numbers to their keycodes.
|
||||
int k0 = KeyEvent.KEYCODE_0;
|
||||
int k1 = KeyEvent.KEYCODE_1;
|
||||
@@ -878,7 +940,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
// When the first digit is 2, the second digit may be 4-5.
|
||||
secondDigit = new Node(k4, k5);
|
||||
firstDigit.addChild(secondDigit);
|
||||
// We must now be followd by the last minute digit. E.g. 2:40, 2:53.
|
||||
// We must now be followed by the last minute digit. E.g. 2:40, 2:53.
|
||||
secondDigit.addChild(minuteSecondDigit);
|
||||
|
||||
// The first digit may be 3-9.
|
||||
@@ -955,20 +1017,24 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
* mLegalKeys represents the keys that can be typed to get to the node.
|
||||
* mChildren are the children that can be reached from this node.
|
||||
*/
|
||||
private class Node {
|
||||
private class Node
|
||||
{
|
||||
private int[] mLegalKeys;
|
||||
private ArrayList<Node> mChildren;
|
||||
|
||||
public Node(int... legalKeys) {
|
||||
public Node(int... legalKeys)
|
||||
{
|
||||
mLegalKeys = legalKeys;
|
||||
mChildren = new ArrayList<Node>();
|
||||
}
|
||||
|
||||
public void addChild(Node child) {
|
||||
public void addChild(Node child)
|
||||
{
|
||||
mChildren.add(child);
|
||||
}
|
||||
|
||||
public boolean containsKey(int key) {
|
||||
public boolean containsKey(int key)
|
||||
{
|
||||
for (int i = 0; i < mLegalKeys.length; i++) {
|
||||
if (mLegalKeys[i] == key) {
|
||||
return true;
|
||||
@@ -977,7 +1043,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
return false;
|
||||
}
|
||||
|
||||
public Node canReach(int key) {
|
||||
public Node canReach(int key)
|
||||
{
|
||||
if (mChildren == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -990,9 +1057,11 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyboardListener implements OnKeyListener {
|
||||
private class KeyboardListener implements OnKeyListener
|
||||
{
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event)
|
||||
{
|
||||
if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
return processKeyUp(keyCode);
|
||||
}
|
||||
@@ -1000,14 +1069,16 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|
||||
}
|
||||
}
|
||||
|
||||
public void setDismissListener( DialogInterface.OnDismissListener listener ) {
|
||||
public void setDismissListener(DialogInterface.OnDismissListener listener)
|
||||
{
|
||||
dismissListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
public void onDismiss(DialogInterface dialog)
|
||||
{
|
||||
super.onDismiss(dialog);
|
||||
if( dismissListener != null )
|
||||
if (dismissListener != null)
|
||||
dismissListener.onDismiss(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,33 +49,33 @@
|
||||
android:layout_height="1dip"
|
||||
android:background="@color/line_background" />
|
||||
|
||||
<LinearLayout
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="@dimen/date_picker_component_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" >
|
||||
|
||||
<Button
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:id="@+id/clear_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/done_background_color"
|
||||
android:minHeight="48dp"
|
||||
android:textColor="#333"
|
||||
android:text="@string/clear_label"
|
||||
android:textColor="@color/done_text_color"
|
||||
android:textSize="@dimen/done_label_size" />
|
||||
|
||||
<Button
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:id="@+id/done_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/done_background_color"
|
||||
android:minHeight="48dp"
|
||||
android:textColor="#333"
|
||||
android:text="@string/done_label"
|
||||
android:textColor="@color/done_text_color"
|
||||
android:textSize="@dimen/done_label_size" />
|
||||
</LinearLayout>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -71,12 +71,12 @@ build_apk() {
|
||||
|
||||
if [ ! -z $RELEASE ]; then
|
||||
log_info "Building release APK"
|
||||
./gradlew assembleRelease
|
||||
$GRADLE assembleRelease
|
||||
cp -v uhabits-android/build/outputs/apk/release/uhabits-android-release.apk build/loop-$VERSION-release.apk
|
||||
fi
|
||||
|
||||
log_info "Building debug APK"
|
||||
./gradlew assembleDebug || fail
|
||||
$GRADLE assembleDebug --stacktrace || fail
|
||||
cp -v uhabits-android/build/outputs/apk/debug/uhabits-android-debug.apk build/loop-$VERSION-debug.apk
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ parse_opts() {
|
||||
}
|
||||
|
||||
remove_build_dir() {
|
||||
rm -rfv .gradle
|
||||
rm -rfv build
|
||||
rm -rfv android-base/build
|
||||
rm -rfv android-pickers/build
|
||||
@@ -231,7 +232,10 @@ case "$1" in
|
||||
|
||||
medium-tests)
|
||||
shift; parse_opts $*
|
||||
run_tests medium
|
||||
for attempt in {1..3}; do
|
||||
(run_tests medium) && exit 0
|
||||
done
|
||||
exit 1
|
||||
;;
|
||||
|
||||
large-tests)
|
||||
@@ -252,7 +256,7 @@ case "$1" in
|
||||
build_apk
|
||||
install_apk
|
||||
;;
|
||||
|
||||
|
||||
clean)
|
||||
remove_build_dir
|
||||
;;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
VERSION_CODE = 52
|
||||
VERSION_NAME = 1.8.9
|
||||
VERSION_CODE = 20000
|
||||
VERSION_NAME = 2.0.0-alpha
|
||||
|
||||
MIN_SDK_VERSION = 21
|
||||
MIN_SDK_VERSION = 23
|
||||
TARGET_SDK_VERSION = 29
|
||||
COMPILE_SDK_VERSION = 29
|
||||
|
||||
DAGGER_VERSION = 2.25.4
|
||||
KOTLIN_VERSION = 1.3.61
|
||||
KOTLIN_VERSION = 1.4.0
|
||||
KX_COROUTINES_VERSION = 1.4.2
|
||||
SUPPORT_LIBRARY_VERSION = 28.0.0
|
||||
AUTO_FACTORY_VERSION = 1.0-beta6
|
||||
BUILD_TOOLS_VERSION = 3.5.3
|
||||
BUILD_TOOLS_VERSION = 4.1.0
|
||||
KTOR_VERSION=1.4.2
|
||||
|
||||
org.gradle.parallel=false
|
||||
org.gradle.daemon=true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#Sat Nov 28 09:55:24 CST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
||||
|
||||
@@ -55,7 +55,7 @@ def get_total(report):
|
||||
|
||||
def get_color(total):
|
||||
"""
|
||||
Return color for current coverage precent
|
||||
Return color for current coverage percent
|
||||
"""
|
||||
try:
|
||||
xtotal = int(total)
|
||||
|
||||
@@ -4,6 +4,7 @@ plugins {
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'com.github.triplet.play' version '2.6.2'
|
||||
id 'kotlin-android-extensions'
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -52,6 +53,7 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
@@ -69,6 +71,10 @@ android {
|
||||
sourceSets {
|
||||
main.assets.srcDirs += '../uhabits-core/src/main/resources/'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -87,7 +93,19 @@ dependencies {
|
||||
implementation "com.google.code.gson:gson:2.8.5"
|
||||
implementation "com.google.code.findbugs:jsr305:3.0.2"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KOTLIN_VERSION"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KX_COROUTINES_VERSION"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$KX_COROUTINES_VERSION"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4"
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation "io.ktor:ktor-client-core:$KTOR_VERSION"
|
||||
implementation "io.ktor:ktor-client-android:$KTOR_VERSION"
|
||||
implementation "io.ktor:ktor-client-json:$KTOR_VERSION"
|
||||
implementation "io.ktor:ktor-client-jackson:$KTOR_VERSION"
|
||||
implementation "com.google.guava:guava:30.0-android"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
compileOnly "javax.annotation:jsr250-api:1.0"
|
||||
compileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
|
||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
@@ -104,7 +122,8 @@ dependencies {
|
||||
androidTestImplementation 'androidx.annotation:annotation:1.0.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.1.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation "com.google.guava:guava:24.1-android"
|
||||
androidTestImplementation "io.ktor:ktor-client-mock:$KTOR_VERSION"
|
||||
androidTestImplementation "io.ktor:ktor-jackson:$KTOR_VERSION"
|
||||
androidTestImplementation project(":uhabits-core")
|
||||
kaptAndroidTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
|
||||
@@ -134,4 +153,4 @@ kapt {
|
||||
play {
|
||||
serviceAccountCredentials = file("../../.secret/gcp-key.json")
|
||||
track = "alpha"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 605 B |
|
Before Width: | Height: | Size: 541 B |
|
Before Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 35 KiB |