Add new Theme "Use System Design" + fixes to night mode (#1069)

* Add theme system

A theme which follows system design.
See: https://www.xda-developers.com/samsung-galaxy-s9-update-night-mode-schedule/

* update

to be in line with https://github.com/tuskyapp/Tusky/pull/1060/files

* Update ThemeUtils.java

* update

* Cleanup

* Update Deps

* Cleanup

* Update PreferencesActivity.kt

* Workaround to make MODE_NIGHT_FOLLOW_SYSTEM work

* Update ThemeUtils.java

* Use ThemeUtils.THEME_SYSTEM

* Update SplashActivity.kt

* Update strings.xml

* Update Deps

* Update build.gradle

* Update build.gradle

* fix tests
This commit is contained in:
Bernd 2019-03-07 21:33:29 +01:00 committed by Konrad Pozniak
parent 006300ede6
commit 507ffb1b41
9 changed files with 45 additions and 41 deletions

View file

@ -86,7 +86,7 @@ dependencies {
implementation 'androidx.browser:browser:1.0.0' implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'com.google.android.material:material:1.1.0-alpha03' implementation 'com.google.android.material:material:1.1.0-alpha04'
implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.1.0-alpha03' implementation 'androidx.preference:preference:1.1.0-alpha03'
@ -117,13 +117,12 @@ dependencies {
kapt 'androidx.room:room-compiler:2.0.0' kapt 'androidx.room:room-compiler:2.0.0'
implementation 'androidx.room:room-rxjava2:2.0.0' implementation 'androidx.room:room-rxjava2:2.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12'
implementation "com.google.dagger:dagger:$daggerVersion" implementation "com.google.dagger:dagger:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion"
implementation "com.google.dagger:dagger-android:$daggerVersion" implementation "com.google.dagger:dagger-android:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion" implementation "com.google.dagger:dagger-android-support:$daggerVersion"
kapt "com.google.dagger:dagger-android-processor:$daggerVersion" kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
testImplementation 'org.robolectric:robolectric:4.1' testImplementation 'org.robolectric:robolectric:4.2'
testImplementation 'org.mockito:mockito-inline:2.24.0' testImplementation 'org.mockito:mockito-inline:2.24.0'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
@ -131,6 +130,7 @@ dependencies {
}) })
androidTestImplementation('android.arch.persistence.room:testing:1.1.1') androidTestImplementation('android.arch.persistence.room:testing:1.1.1')
androidTestImplementation "androidx.test.ext:junit:1.1.0" androidTestImplementation "androidx.test.ext:junit:1.1.0"
testImplementation "androidx.test.ext:junit:1.1.0"
debugImplementation 'im.dino:dbinspector:3.4.1@aar' debugImplementation 'im.dino:dbinspector:3.4.1@aar'
implementation 'io.reactivex.rxjava2:rxjava:2.2.6' implementation 'io.reactivex.rxjava2:rxjava:2.2.6'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'

View file

@ -56,6 +56,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
@Inject @Inject
public AccountManager accountManager; public AccountManager accountManager;
ThemeUtils themeUtils = new ThemeUtils();
protected static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; protected static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
@Override @Override
@ -72,7 +74,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
if (theme.equals("black")) { if (theme.equals("black")) {
setTheme(R.style.TuskyBlackTheme); setTheme(R.style.TuskyBlackTheme);
} }
ThemeUtils.setAppNightMode(theme, this);
themeUtils.setAppNightMode(theme, this);
/* set the taskdescription programmatically, the theme would turn it blue */ /* set the taskdescription programmatically, the theme would turn it blue */
String appName = getString(R.string.app_name); String appName = getString(R.string.app_name);

View file

@ -34,6 +34,7 @@ import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.toolbar_basic.* import kotlinx.android.synthetic.main.toolbar_basic.*
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import javax.inject.Inject import javax.inject.Inject
import androidx.appcompat.app.AppCompatDelegate
class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener, HasSupportFragmentInjector { class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener, HasSupportFragmentInjector {
@ -123,7 +124,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
"appTheme" -> { "appTheme" -> {
val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT) val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
Log.d("activeTheme", theme) Log.d("activeTheme", theme)
ThemeUtils.setAppNightMode(theme, this) ThemeUtils().setAppNightMode(theme, this)
restartActivitiesOnExit = true restartActivitiesOnExit = true
// recreate() could be used instead, but it doesn't have an animation B). // recreate() could be used instead, but it doesn't have an animation B).
@ -135,7 +136,13 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
finish() finish()
overridePendingTransition(R.anim.fade_in, R.anim.fade_out) overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
restartActivitiesOnExit = true // MODE_NIGHT_FOLLOW_SYSTEM workaround part 2 :/
when(theme){
ThemeUtils.THEME_SYSTEM -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
//workaround end
} }
"statusTextSize" -> { "statusTextSize" -> {

View file

@ -17,18 +17,10 @@ package com.keylesspalace.tusky
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NotificationHelper import com.keylesspalace.tusky.util.NotificationHelper
import javax.inject.Inject class SplashActivity : BaseActivity() {
class SplashActivity : AppCompatActivity(), Injectable {
@Inject
lateinit var accountManager: AccountManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -106,14 +106,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
private void setConversationName(List<ConversationAccountEntity> accounts) { private void setConversationName(List<ConversationAccountEntity> accounts) {
Context context = conversationNameTextView.getContext(); Context context = conversationNameTextView.getContext();
String conversationName; String conversationName = "";
if(accounts.size() == 0) { if(accounts.size() == 1) {
conversationName = " ";
}else if(accounts.size() == 1) {
conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername()); conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername());
} else if(accounts.size() == 2) { } else if(accounts.size() == 2) {
conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername()); conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername());
} else { } else if (accounts.size() > 2){
conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2); conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2);
} }

View file

@ -15,12 +15,10 @@
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.util;
import android.app.UiModeManager;
import android.content.Context; import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.AttrRes; import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes; import androidx.annotation.ColorRes;
@ -28,8 +26,8 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import android.provider.Settings;
import android.util.TypedValue; import android.util.TypedValue;
import android.widget.ImageView;
/** /**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where * Provides runtime compatibility to obtain theme information and re-theme views, especially where
@ -42,6 +40,7 @@ public class ThemeUtils {
private static final String THEME_DAY = "day"; private static final String THEME_DAY = "day";
private static final String THEME_BLACK = "black"; private static final String THEME_BLACK = "black";
private static final String THEME_AUTO = "auto"; private static final String THEME_AUTO = "auto";
public static final String THEME_SYSTEM = "auto_system";
public static Drawable getDrawable(@NonNull Context context, @AttrRes int attribute, public static Drawable getDrawable(@NonNull Context context, @AttrRes int attribute,
@DrawableRes int fallbackDrawable) { @DrawableRes int fallbackDrawable) {
@ -85,10 +84,6 @@ public class ThemeUtils {
ResourcesUtils.getResourceIdentifier(context, "attr", name)); ResourcesUtils.getResourceIdentifier(context, "attr", name));
} }
public static void setImageViewTint(ImageView view, @AttrRes int attribute) {
view.setColorFilter(getColor(view.getContext(), attribute), PorterDuff.Mode.SRC_IN);
}
/** this can be replaced with drawableTint in xml once minSdkVersion >= 23 */ /** this can be replaced with drawableTint in xml once minSdkVersion >= 23 */
public static @Nullable Drawable getTintedDrawable(@NonNull Context context, @DrawableRes int drawableId, @AttrRes int colorAttr) { public static @Nullable Drawable getTintedDrawable(@NonNull Context context, @DrawableRes int drawableId, @AttrRes int colorAttr) {
Drawable drawable = context.getDrawable(drawableId); Drawable drawable = context.getDrawable(drawableId);
@ -103,30 +98,31 @@ public class ThemeUtils {
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN); drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
} }
public static void setAppNightMode(String flavor, Context context) { public void setAppNightMode(String flavor, Context context) {
int mode;
switch (flavor) { switch (flavor) {
default: default:
case THEME_NIGHT: case THEME_NIGHT:
mode = UiModeManager.MODE_NIGHT_YES; AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break; break;
case THEME_DAY: case THEME_DAY:
mode = UiModeManager.MODE_NIGHT_NO; AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break; break;
case THEME_BLACK: case THEME_BLACK:
mode = UiModeManager.MODE_NIGHT_YES; AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break; break;
case THEME_AUTO: case THEME_AUTO:
mode = UiModeManager.MODE_NIGHT_AUTO; AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);
break;
case THEME_SYSTEM:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
//stupid workaround to make MODE_NIGHT_FOLLOW_SYSTEM work :(
if((Settings.System.getInt(context.getContentResolver(), "display_night_theme", 0) == 1)) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else if ((Settings.System.getInt(context.getContentResolver(), "display_night_theme", 0) == 0)) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
break; break;
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
UiModeManager uiModeManager = (UiModeManager)context.getApplicationContext().getSystemService(Context.UI_MODE_SERVICE);
uiModeManager.setNightMode(mode);
} else {
AppCompatDelegate.setDefaultNightMode(mode);
}
} }
} }

View file

@ -28,6 +28,7 @@
<item>day</item> <item>day</item>
<item>black</item> <item>black</item>
<item>auto</item> <item>auto</item>
<item>auto_system</item>
</string-array> </string-array>
</resources> </resources>

View file

@ -205,6 +205,7 @@
<item>Light</item> <item>Light</item>
<item>Black</item> <item>Black</item>
<item>Automatic at sunset</item> <item>Automatic at sunset</item>
<item>Use System Design</item>
</string-array> </string-array>
<string name="pref_title_browser_settings">Browser</string> <string name="pref_title_browser_settings">Browser</string>

View file

@ -26,6 +26,7 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils
import okhttp3.Request import okhttp3.Request
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.junit.Assert import org.junit.Assert
@ -44,6 +45,7 @@ import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
/** /**
* Created by charlag on 3/7/18. * Created by charlag on 3/7/18.
*/ */
@ -55,6 +57,7 @@ class ComposeActivityTest {
lateinit var activity: ComposeActivity lateinit var activity: ComposeActivity
lateinit var accountManagerMock: AccountManager lateinit var accountManagerMock: AccountManager
lateinit var apiMock: MastodonApi lateinit var apiMock: MastodonApi
lateinit var themeUtilsMock: ThemeUtils
val account = AccountEntity( val account = AccountEntity(
id = 1, id = 1,
@ -135,9 +138,12 @@ class ComposeActivityTest {
val dbMock = mock(AppDatabase::class.java) val dbMock = mock(AppDatabase::class.java)
`when`(dbMock.instanceDao()).thenReturn(instanceDaoMock) `when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
themeUtilsMock = Mockito.mock(ThemeUtils::class.java)
activity.mastodonApi = apiMock activity.mastodonApi = apiMock
activity.accountManager = accountManagerMock activity.accountManager = accountManagerMock
activity.database = dbMock activity.database = dbMock
activity.themeUtils = themeUtilsMock
`when`(accountManagerMock.activeAccount).thenReturn(account) `when`(accountManagerMock.activeAccount).thenReturn(account)