added launch presentation related code

This commit is contained in:
Mariotaku Lee 2017-08-22 15:39:47 +08:00
parent 5afe8c3a37
commit 7c19db15d3
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
36 changed files with 1475 additions and 84 deletions

View File

@ -32,7 +32,7 @@ allprojects {
subprojects {
buildscript {
ext {
kotlinVersion = '1.1.3-2'
kotlinVersion = '1.1.4-2'
pluginVersions = [
AndroidSvgDrawable: '3.0.0',
Fabric : '1.22.1',
@ -40,10 +40,10 @@ subprojects {
PlayServices : '3.1.0',
]
libVersions = [
Kotlin : '1.1.3-2',
Kotlin : '1.1.4-2',
SupportLib : '26.0.1',
SupportTest : '1.0.0',
MariotakuCommons : '0.9.15',
MariotakuCommons : '0.9.17',
RestFu : '0.9.57',
ObjectCursor : '0.9.20',
PlayServices : '11.0.4',

View File

@ -198,7 +198,7 @@ dependencies {
implementation "com.github.mariotaku.CommonsLibrary:io:${libVersions['MariotakuCommons']}"
implementation "com.github.mariotaku.CommonsLibrary:text:${libVersions['MariotakuCommons']}"
implementation "com.github.mariotaku.CommonsLibrary:text-kotlin:${libVersions['MariotakuCommons']}"
implementation "com.github.mariotaku.CommonsLibrary:emojione:${libVersions['MariotakuCommons']}"
implementation "com.github.mariotaku.CommonsLibrary:emojione-android:${libVersions['MariotakuCommons']}"
implementation "com.github.mariotaku.CommonsLibrary:objectcursor:${libVersions['MariotakuCommons']}"
implementation "com.github.mariotaku.CommonsLibrary:logansquare:${libVersions['MariotakuCommons']}"
implementation "com.github.mariotaku:KPreferences:${libVersions['KPreferences']}"

View File

@ -0,0 +1,172 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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.mariotaku.twidere.util;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.Choreographer;
import java.util.Map;
import java.util.WeakHashMap;
/**
* Created by mariotaku on 2017/8/20.
*/
public abstract class ChoreographerCompat {
private ChoreographerCompat() {
}
public abstract void postFrameCallback(ChoreographerCompat.FrameCallback callback);
public abstract void postFrameCallbackDelayed(ChoreographerCompat.FrameCallback callback, long delayMillis);
public abstract void removeFrameCallback(ChoreographerCompat.FrameCallback callback);
public static ChoreographerCompat getInstance() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return ChoreographerCompatImpl.getCompatInstance();
}
return ChoreographerNativeDelegate.getNativeInstance();
}
public interface FrameCallback {
void doFrame(long frameTimeNanos);
}
private static class ChoreographerCompatImpl extends ChoreographerCompat {
private static final Map<Looper, ChoreographerCompat> sInstances = new WeakHashMap<>();
private final Handler handler;
ChoreographerCompatImpl(Looper looper) {
handler = new Handler(looper);
}
@Override
public void postFrameCallback(FrameCallback callback) {
handler.postDelayed(CompatFrameCallbackWrapper.wrap(callback), 0);
}
@Override
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
handler.postDelayed(CompatFrameCallbackWrapper.wrap(callback), delayMillis + 16);
}
@Override
public void removeFrameCallback(FrameCallback callback) {
handler.removeCallbacks(CompatFrameCallbackWrapper.wrap(callback));
}
static ChoreographerCompat getCompatInstance() {
final Looper looper = Looper.myLooper();
ChoreographerCompat instance = sInstances.get(looper);
if (instance != null) return instance;
instance = new ChoreographerCompatImpl(looper);
sInstances.put(looper, instance);
return instance;
}
private static class CompatFrameCallbackWrapper implements Runnable {
private static final Map<FrameCallback, Runnable> sInstances = new WeakHashMap<>();
private final FrameCallback callback;
private CompatFrameCallbackWrapper(ChoreographerCompat.FrameCallback callback) {
this.callback = callback;
}
@Override
public void run() {
callback.doFrame(System.nanoTime());
}
static Runnable wrap(FrameCallback callback) {
Runnable wrapper = sInstances.get(callback);
if (wrapper != null) return wrapper;
wrapper = new CompatFrameCallbackWrapper(callback);
sInstances.put(callback, wrapper);
return wrapper;
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static class ChoreographerNativeDelegate extends ChoreographerCompat {
private static final Map<Choreographer, ChoreographerCompat> sInstances = new WeakHashMap<>();
private final Choreographer implementation;
ChoreographerNativeDelegate(Choreographer implementation) {
this.implementation = implementation;
}
public void postFrameCallback(ChoreographerCompat.FrameCallback callback) {
implementation.postFrameCallback(NativeFrameCallbackWrapper.wrap(callback));
}
public void postFrameCallbackDelayed(ChoreographerCompat.FrameCallback callback, long delayMillis) {
implementation.postFrameCallbackDelayed(NativeFrameCallbackWrapper.wrap(callback), delayMillis);
}
public void removeFrameCallback(ChoreographerCompat.FrameCallback callback) {
implementation.removeFrameCallback(NativeFrameCallbackWrapper.wrap(callback));
}
static ChoreographerCompat getNativeInstance() {
final Choreographer implementation = Choreographer.getInstance();
ChoreographerCompat instance = sInstances.get(implementation);
if (instance != null) return instance;
instance = new ChoreographerNativeDelegate(implementation);
sInstances.put(implementation, instance);
return instance;
}
private static class NativeFrameCallbackWrapper implements Choreographer.FrameCallback {
private static final Map<FrameCallback, Choreographer.FrameCallback> sInstances = new WeakHashMap<>();
private final FrameCallback callback;
private NativeFrameCallbackWrapper(ChoreographerCompat.FrameCallback callback) {
this.callback = callback;
}
@Override
public void doFrame(long frameTimeNanos) {
callback.doFrame(frameTimeNanos);
}
static Choreographer.FrameCallback wrap(ChoreographerCompat.FrameCallback callback) {
Choreographer.FrameCallback wrapper = sInstances.get(callback);
if (wrapper != null) return wrapper;
wrapper = new NativeFrameCallbackWrapper(callback);
sInstances.put(callback, wrapper);
return wrapper;
}
}
}
}

View File

@ -20,6 +20,7 @@
package org.mariotaku.twidere.activity
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
@ -54,13 +55,10 @@ class ComposeActivityTest {
intent.putExtra(EXTRA_STATUS, status)
intent.putExtra(EXTRA_SAVE_DRAFT, true)
val activity = activityRule.launchActivity(intent)
val getStatusUpdate = activity.javaClass.getDeclaredMethod("getStatusUpdate").apply {
isAccessible = true
}
activityRule.runOnUiThread {
activity.editText.setText("@t_deyarmin @nixcraft @mariotaku Test Reply")
}
val statusUpdate = getStatusUpdate(activity) as ParcelableStatusUpdate
val statusUpdate = activity.getStatusUpdateTest(false)
Assert.assertEquals("Test Reply", statusUpdate.text)
assertExcludedMatches(emptyArray(), statusUpdate)
activity.finish()
@ -76,13 +74,10 @@ class ComposeActivityTest {
intent.putExtra(EXTRA_STATUS, status)
intent.putExtra(EXTRA_SAVE_DRAFT, true)
val activity = activityRule.launchActivity(intent)
val getStatusUpdate = activity.javaClass.getDeclaredMethod("getStatusUpdate").apply {
isAccessible = true
}
activityRule.runOnUiThread {
activity.editText.setText("@t_deyarmin Test Reply")
}
val statusUpdate = getStatusUpdate(activity) as ParcelableStatusUpdate
val statusUpdate = activity.getStatusUpdateTest(false)
Assert.assertEquals("Test Reply", statusUpdate.text)
assertExcludedMatches(arrayOf("17484680", "57610574"), statusUpdate)
activity.finish()
@ -98,13 +93,10 @@ class ComposeActivityTest {
intent.putExtra(EXTRA_STATUS, status)
intent.putExtra(EXTRA_SAVE_DRAFT, true)
val activity = activityRule.launchActivity(intent)
val getStatusUpdate = activity.javaClass.getDeclaredMethod("getStatusUpdate").apply {
isAccessible = true
}
activityRule.runOnUiThread {
activity.editText.setText("Test Reply")
}
val statusUpdate = getStatusUpdate(activity) as ParcelableStatusUpdate
val statusUpdate = activity.getStatusUpdateTest(false)
Assert.assertEquals("Test Reply", statusUpdate.text)
Assert.assertEquals("https://twitter.com/t_deyarmin/status/847950697987493888",
statusUpdate.attachment_url)
@ -122,13 +114,10 @@ class ComposeActivityTest {
intent.putExtra(EXTRA_STATUS, status)
intent.putExtra(EXTRA_SAVE_DRAFT, true)
val activity = activityRule.launchActivity(intent)
val getStatusUpdate = activity.javaClass.getDeclaredMethod("getStatusUpdate").apply {
isAccessible = true
}
activityRule.runOnUiThread {
activity.editText.setText("@TwidereProject @mariotaku Test Reply")
}
val statusUpdate = getStatusUpdate(activity) as ParcelableStatusUpdate
val statusUpdate = activity.getStatusUpdateTest(false)
Assert.assertEquals("Test Reply", statusUpdate.text)
assertExcludedMatches(emptyArray(), statusUpdate)
activity.finish()
@ -144,13 +133,10 @@ class ComposeActivityTest {
intent.putExtra(EXTRA_STATUS, status)
intent.putExtra(EXTRA_SAVE_DRAFT, true)
val activity = activityRule.launchActivity(intent)
val getStatusUpdate = activity.javaClass.getDeclaredMethod("getStatusUpdate").apply {
isAccessible = true
}
activityRule.runOnUiThread {
activity.editText.setText("@TwidereProject Test Reply")
}
val statusUpdate = getStatusUpdate(activity) as ParcelableStatusUpdate
val statusUpdate = activity.getStatusUpdateTest(false)
Assert.assertEquals("Test Reply", statusUpdate.text)
assertExcludedMatches(arrayOf("57610574"), statusUpdate)
activity.finish()
@ -166,18 +152,23 @@ class ComposeActivityTest {
intent.putExtra(EXTRA_STATUS, status)
intent.putExtra(EXTRA_SAVE_DRAFT, true)
val activity = activityRule.launchActivity(intent)
val getStatusUpdate = activity.javaClass.getDeclaredMethod("getStatusUpdate").apply {
isAccessible = true
}
activityRule.runOnUiThread {
activity.editText.setText("Test Reply")
}
val statusUpdate = getStatusUpdate(activity) as ParcelableStatusUpdate
val statusUpdate = activity.getStatusUpdateTest(false)
Assert.assertEquals("Test Reply", statusUpdate.text)
assertExcludedMatches(arrayOf("583328497", "57610574"), statusUpdate)
activity.finish()
}
private fun Activity.getStatusUpdateTest(checkLength: Boolean): ParcelableStatusUpdate {
val getStatusUpdate = javaClass.getDeclaredMethod("getStatusUpdate",
kotlin.Boolean::class.java).apply {
isAccessible = true
}
return getStatusUpdate(this, checkLength) as ParcelableStatusUpdate
}
private fun assertExcludedMatches(expectedIds: Array<String>, statusUpdate: ParcelableStatusUpdate): Boolean {
return statusUpdate.excluded_reply_user_ids?.all { excludedId ->
expectedIds.any { expectation ->

View File

@ -0,0 +1,71 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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.mariotaku.twidere.model
import org.junit.Assert
import org.junit.Test
import java.util.*
/**
* Created by mariotaku on 2017/8/19.
*/
class CronExpressionTest {
@Test
fun testMatches() {
// @daily (0:00 every day)
val cal0h0m = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
}
// Every 1:00, 15th day of month
val cal15dom1h0m = Calendar.getInstance(TimeZone.getTimeZone("JST")).apply {
set(Calendar.HOUR_OF_DAY, 1)
set(Calendar.MINUTE, 0)
set(Calendar.DAY_OF_MONTH, 15)
}
Assert.assertTrue(CronExpression.valueOf("0 0 * * *").matches(cal0h0m))
Assert.assertFalse(CronExpression.valueOf("0 0 * * *").matches(cal15dom1h0m))
// Here comes the timezone related part
Assert.assertTrue(CronExpression.valueOf("0 1 15 * *").matches(cal15dom1h0m))
}
class FieldTest {
@Test
fun testParseBasic() {
Assert.assertSame(CronExpression.AnyField.INSTANCE, CronExpression.FieldType.MINUTE.parseField("*"))
Assert.assertArrayEquals(arrayOf(CronExpression.Range.single(0), CronExpression.Range.single(1)),
(CronExpression.FieldType.DAY_OF_WEEK.parseField("SUN,MON") as CronExpression.BasicField).ranges)
Assert.assertArrayEquals(arrayOf(CronExpression.Range.single(0), CronExpression.Range(1, 5)),
(CronExpression.FieldType.DAY_OF_WEEK.parseField("SUN,MON-FRI") as CronExpression.BasicField).ranges)
}
}
class RangeTest {
@Test
fun testParse() {
Assert.assertEquals(CronExpression.Range(0, 6), CronExpression.Range.parse("0-6", CronExpression.Range(0, 10), null))
Assert.assertEquals(CronExpression.Range.single(0), CronExpression.Range.parse("SUN", CronExpression.Range(0, 7), CronExpression.FieldType.DAY_OF_WEEK.textRepresentations))
Assert.assertEquals(CronExpression.Range(0, 3), CronExpression.Range.parse("SUN-WED", CronExpression.Range(0, 7), CronExpression.FieldType.DAY_OF_WEEK.textRepresentations))
}
}
}

View File

@ -1,20 +0,0 @@
package org.mariotaku.twidere.util
import android.support.test.runner.AndroidJUnit4
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.mariotaku.twidere.BuildConfig
/**
* Created by mariotaku on 2016/12/15.
*/
@RunWith(AndroidJUnit4::class)
class AnalyzerTest {
@Test
fun testGetInstance() {
if (BuildConfig.FLAVOR.contains("google")) {
Assert.assertNotNull(Analyzer.implementation)
}
}
}

View File

@ -6,6 +6,7 @@ import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.mariotaku.twidere.model.filter.UrlFiltersSubscriptionProviderArguments
/**
* Created by mariotaku on 2017/1/11.
@ -20,7 +21,7 @@ class UrlFiltersSubscriptionProviderTest {
if (!(cm.activeNetworkInfo?.isConnected ?: false)) return
val url = "https://raw.githubusercontent.com/mariotaku/wtb/master/twidere/bots.xml"
val arguments = UrlFiltersSubscriptionProvider.Arguments().apply {
val arguments = UrlFiltersSubscriptionProviderArguments().apply {
this.url = url
}
val provider = UrlFiltersSubscriptionProvider(context, arguments)

View File

@ -161,7 +161,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/Theme.Twidere.NoActionBar"
android:theme="@style/Theme.Twidere.Launcher"
android:windowSoftInputMode="adjustNothing">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@ -181,7 +181,7 @@
android:icon="@mipmap/ic_launcher_hondajojo"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/Theme.Twidere.NoActionBar"
android:theme="@style/Theme.Twidere.Launcher"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

View File

@ -0,0 +1,417 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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.mariotaku.twidere.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import java.text.ParseException;
import java.util.Calendar;
/**
* Single-file Cron expression parser
* <p>
* Supports POSIX standard syntax only
*/
public class CronExpression {
/**
* Equivalent to {@code 0 0 1 1 *}
*/
public static final CronExpression YEARLY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
BasicField.zero(FieldType.HOUR_OF_DAY),
BasicField.one(FieldType.DAY_OF_MONTH),
BasicField.one(FieldType.MONTH),
AnyField.INSTANCE,
null,
});
public static final CronExpression ANNUALLY = YEARLY;
/**
* Equivalent to {@code 0 0 1 * *}
*/
public static final CronExpression MONTHLY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
BasicField.zero(FieldType.HOUR_OF_DAY),
BasicField.one(FieldType.DAY_OF_MONTH),
AnyField.INSTANCE,
AnyField.INSTANCE,
null,
});
/**
* Equivalent to {@code 0 0 * * 0}
*/
public static final CronExpression WEEKLY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
BasicField.zero(FieldType.HOUR_OF_DAY),
AnyField.INSTANCE,
AnyField.INSTANCE,
BasicField.zero(FieldType.DAY_OF_WEEK),
null,
});
/**
* Equivalent to {@code 0 0 * * *}
*/
public static final CronExpression DAILY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
BasicField.zero(FieldType.HOUR_OF_DAY),
AnyField.INSTANCE,
AnyField.INSTANCE,
AnyField.INSTANCE,
null,
});
/**
* Equivalent to {@code 0 * * * *}
*/
public static final CronExpression HOURLY = new CronExpression(new Field[]{
BasicField.zero(FieldType.MINUTE),
AnyField.INSTANCE,
AnyField.INSTANCE,
AnyField.INSTANCE,
AnyField.INSTANCE,
null,
});
private Field[] fields;
private CronExpression(@NonNull Field[] fields) {
if (fields.length < 5) throw new IllegalArgumentException("Fields count must >= 5");
this.fields = fields;
}
@NonNull
public static CronExpression valueOf(@NonNull String string) throws ParseException {
if (string.length() == 0) {
throw new ParseException("Cron expression is empty", -1);
}
if (string.charAt(0) == '@') {
// Parse predefined
final String substr = string.substring(1);
switch (substr) {
case "yearly":
return YEARLY;
case "annually":
return ANNUALLY;
case "monthly":
return MONTHLY;
case "weekly":
return WEEKLY;
case "daily":
return DAILY;
case "hourly":
return HOURLY;
}
throw new ParseException("Unknown pre-defined value " + substr, 1);
}
final String[] segments = StringUtils.split(string, ' ');
if (segments.length > 6) {
throw new ParseException("Unrecognized segments " + string, -1);
}
// Parse minute field
Field[] fields = new Field[6];
fields[0] = FieldType.MINUTE.parseField(segments[0]);
// Parse hour field
fields[1] = FieldType.HOUR_OF_DAY.parseField(segments[1]);
// Parse day-of-month field
fields[2] = FieldType.DAY_OF_MONTH.parseField(segments[2]);
// Parse month field
fields[3] = FieldType.MONTH.parseField(segments[3]);
// Parse day-of-week field
fields[4] = FieldType.DAY_OF_WEEK.parseField(segments[4]);
return new CronExpression(fields);
}
public boolean matches(Calendar cal) {
for (Field field : fields) {
if (field == null) continue;
if (!field.contains(cal)) return false;
}
return true;
}
public String toExpression() {
StringBuilder sb = new StringBuilder();
for (int i = 0, j = fields.length; i < j; i++) {
Field field = fields[i];
if (field == null) continue;
if (i != 0) {
sb.append(' ');
}
sb.append(field.toExpression());
}
return sb.toString();
}
interface Field {
boolean contains(Calendar cal);
String toExpression();
}
public enum FieldType {
MINUTE(Calendar.MINUTE, 0, new Range(0, 59), null),
HOUR_OF_DAY(Calendar.HOUR_OF_DAY, 0, new Range(0, 23), null),
DAY_OF_MONTH(Calendar.DAY_OF_MONTH, 0, new Range(1, 31), null),
MONTH(Calendar.MONTH, 1, new Range(1, 12), new String[]{"JAN", "FEB", "MAR", "APR", "JUN", "JUL",
"AUG", "SEP", "OCT", "NOV", "DEC"}),
DAY_OF_WEEK(Calendar.DAY_OF_WEEK, -1, new Range(0, 6), new String[]{"SUN", "MON", "TUE", "WED", "THU",
"FRI", "SAT"}),
/* Used in nncron, not decided whether to implement */
YEAR(Calendar.YEAR, 0, new Range(1970, 2099), null);
final int calendarField;
final int calendarOffset;
final Range allowedRange;
final String[] textRepresentations;
FieldType(int calendarField, int calendarOffset, Range allowedRange,
@Nullable String[] textRepresentations) {
this.calendarField = calendarField;
this.calendarOffset = calendarOffset;
this.allowedRange = allowedRange;
this.textRepresentations = textRepresentations;
}
Field parseField(@NonNull String text) throws ParseException {
if ("*".equals(text)) return AnyField.INSTANCE;
return BasicField.parse(text, this);
}
}
enum AnyField implements Field {
INSTANCE;
@Override
public boolean contains(Calendar cal) {
return true;
}
@Override
public String toExpression() {
return "*";
}
}
/**
* POSIX-compliant CRON field
*/
final static class BasicField implements Field {
@NonNull
final Range[] ranges;
final int calendarField;
final int calendarOffset;
BasicField(@NonNull final Range[] ranges, int calendarField, int calendarOffset) {
this.ranges = ranges;
this.calendarField = calendarField;
this.calendarOffset = calendarOffset;
}
public static Field parse(String text, FieldType fieldType) throws ParseException {
final Range allowedRange = fieldType.allowedRange;
final String[] rangeStrings = StringUtils.split(text, ',');
final Range[] ranges = new Range[rangeStrings.length];
final String[] textRepresentations = fieldType.textRepresentations;
for (int i = 0, l = rangeStrings.length; i < l; i++) {
String rangeString = rangeStrings[i];
ranges[i] = Range.parse(rangeString, allowedRange, textRepresentations);
if (!allowedRange.contains(ranges[i])) {
throw new ParseException(ranges[i] + " out of range " + allowedRange, -1);
}
}
return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset);
}
public static Field zero(FieldType fieldType) {
final Range[] ranges = {Range.single(0)};
return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset);
}
public static Field one(FieldType fieldType) {
final Range[] ranges = {Range.single(1)};
return new BasicField(ranges, fieldType.calendarField, fieldType.calendarOffset);
}
@Override
public boolean contains(Calendar cal) {
final int cmp = cal.get(calendarField) + calendarOffset;
for (Range range : ranges) {
if (range.contains(cmp)) return true;
}
return false;
}
@Override
public String toExpression() {
StringBuilder sb = new StringBuilder();
for (int i = 0, j = ranges.length; i < j; i++) {
Range range = ranges[i];
if (i != 0) {
sb.append(',');
}
sb.append(range.toExpression());
}
return sb.toString();
}
}
final static class DayOfMonthField implements Field {
static Field parse(String text, @NonNull Range allowedRange,
@Nullable String[] textRepresentations) {
throw new UnsupportedOperationException();
}
@Override
public boolean contains(Calendar cal) {
throw new UnsupportedOperationException();
}
@Override
public String toExpression() {
throw new UnsupportedOperationException();
}
}
final static class DayOfWeekField implements Field {
public static Field parse(String text, @NonNull Range allowedRange,
@Nullable String[] textRepresentations) {
throw new UnsupportedOperationException();
}
@Override
public boolean contains(Calendar cal) {
throw new UnsupportedOperationException();
}
@Override
public String toExpression() {
throw new UnsupportedOperationException();
}
}
final static class Range {
final int start, endInclusive;
Range(int start, int endInclusive) {
if (endInclusive < start) {
throw new IllegalArgumentException("endInclusive < start");
}
this.start = start;
this.endInclusive = endInclusive;
}
public boolean contains(int num) {
return num >= start && num <= endInclusive;
}
public boolean contains(Range that) {
return this.start <= that.start && this.endInclusive >= that.endInclusive;
}
public int size() {
return endInclusive - start + 1;
}
public int valueAt(int index) {
if (index >= size()) {
throw new IndexOutOfBoundsException("Range index out of bounds");
}
return start + index;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Range range = (Range) o;
if (start != range.start) return false;
return endInclusive == range.endInclusive;
}
@Override
public int hashCode() {
int result = start;
result = 31 * result + endInclusive;
return result;
}
@Override
public String toString() {
return "Range{" +
"start=" + start +
", endInclusive=" + endInclusive +
'}';
}
public static Range single(int num) {
return new Range(num, num);
}
public static Range parse(String string, Range allowedRange,
@Nullable String[] textRepresentations) throws ParseException {
int dashIdx = string.indexOf('-');
if (dashIdx == -1) {
return single(parseNumber(string, allowedRange, textRepresentations));
}
final int start = parseNumber(string.substring(0, dashIdx), allowedRange,
textRepresentations);
final int endInclusive = parseNumber(string.substring(dashIdx + 1, string.length()),
allowedRange, textRepresentations);
return new Range(start, endInclusive);
}
private static int parseNumber(String input, Range allowedRange,
@Nullable String[] textRepresentations) throws ParseException {
if (textRepresentations != null) {
int textRepresentationIndex = ArrayUtils.indexOf(textRepresentations, input);
if (textRepresentationIndex != ArrayUtils.INDEX_NOT_FOUND) {
return allowedRange.valueAt(textRepresentationIndex);
}
}
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new ParseException(e.getMessage(), -1);
}
}
public String toExpression() {
if (endInclusive == start) return Integer.toString(start);
return start + "-" + endInclusive;
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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.mariotaku.twidere.model.filter;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
/**
* Created by mariotaku on 2017/8/20.
*/
@JsonObject
public class UrlFiltersSubscriptionProviderArguments {
@JsonField(name = "url")
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}

View File

@ -0,0 +1,150 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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.mariotaku.twidere.model.presentation;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import org.mariotaku.commons.logansquare.StringBasedListTypeConverter;
import org.mariotaku.twidere.model.CronExpression;
import org.mariotaku.twidere.model.util.CronExpressionConverter;
import java.util.List;
/**
* Created by mariotaku on 2017/8/16.
*/
@JsonObject
public class LaunchPresentation {
@JsonField(name = "images")
List<Image> images;
@JsonField(name = "locales", typeConverter = Locale.ListConverter.class)
List<Locale> locales;
@JsonField(name = "url")
String url;
@JsonField(name = "schedule")
Schedule schedule;
@JsonField(name = "is_promotion")
boolean isPromotion;
public List<Image> getImages() {
return images;
}
public List<Locale> getLocales() {
return locales;
}
public String getUrl() {
return url;
}
public Schedule getSchedule() {
return schedule;
}
public boolean isPromotion() {
return isPromotion;
}
@JsonObject
public static class Schedule {
@JsonField(name = "cron", typeConverter = CronExpressionConverter.class)
CronExpression cron;
@JsonField(name = "local")
boolean local;
public CronExpression getCron() {
return cron;
}
public boolean isLocal() {
return local;
}
}
public static class Locale {
@NonNull
final String language;
@Nullable
final String country;
public Locale(@NonNull String language, @Nullable String country) {
this.language = language;
this.country = country;
}
@NonNull
public String getLanguage() {
return language;
}
@Nullable
public String getCountry() {
return country;
}
@Override
public String toString() {
if (country == null) return language;
return language + "-" + country;
}
public static Locale valueOf(@NonNull String str) {
int dashIndex = str.indexOf('-');
if (dashIndex == -1) return new Locale(str, null);
return new Locale(str.substring(0, dashIndex),
str.substring(dashIndex + 1, str.length()));
}
static class ListConverter extends StringBasedListTypeConverter<Locale> {
@Override
public Locale getItemFromString(String str) {
if (str == null) return null;
return Locale.valueOf(str);
}
@Override
public String convertItemToString(Locale item) {
if (item == null) return null;
return item.toString();
}
}
}
@JsonObject
public static class Image {
@JsonField(name = "url")
String url;
public String getUrl() {
return url;
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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.mariotaku.twidere.model.util;
import com.bluelinelabs.logansquare.typeconverters.TypeConverter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import org.mariotaku.twidere.model.CronExpression;
import java.io.IOException;
import java.text.ParseException;
/**
* Created by mariotaku on 2017/8/19.
*/
public class CronExpressionConverter implements TypeConverter<CronExpression> {
@Override
public CronExpression parse(JsonParser jsonParser) throws IOException {
final String string = jsonParser.getValueAsString(null);
if (string == null) return null;
try {
return CronExpression.valueOf(string);
} catch (ParseException e) {
throw new IOException(e);
}
}
@Override
public void serialize(CronExpression object, String fieldName, boolean writeFieldNameForObject,
JsonGenerator jsonGenerator) throws IOException {
if (object == null) {
if (fieldName != null) {
jsonGenerator.writeNullField(fieldName);
} else {
jsonGenerator.writeNull();
}
} else {
if (fieldName != null) {
jsonGenerator.writeStringField(fieldName, object.toExpression());
} else {
jsonGenerator.writeString(object.toExpression());
}
}
}
}

View File

@ -1,8 +1,10 @@
package org.mariotaku.ktextension
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
/**
@ -25,4 +27,10 @@ fun Context.unregisterReceiverSafe(receiver: BroadcastReceiver?): Boolean {
} catch (e: IllegalArgumentException) {
return false
}
}
}
val Context.componentIcon: Drawable?
get() {
val info = packageManager.getActivityInfo(ComponentName(this, javaClass), 0)
return info.loadIcon(packageManager)
}

View File

@ -145,6 +145,9 @@ open class BaseActivity : ChameleonActivity(), IBaseActivity<BaseActivity>, IThe
override val themeBackgroundOption: String
get() = themePreferences[themeBackgroundOptionKey]
open val themeNavigationStyle: String
get() = themePreferences[navbarStyleKey]
private var isNightBackup: Int = TwilightManagerAccessor.UNSPECIFIED
private val actionHelper = IBaseActivity.ActionHelper(this)
@ -237,13 +240,12 @@ open class BaseActivity : ChameleonActivity(), IBaseActivity<BaseActivity>, IThe
StrictModeUtils.detectAllVmPolicy()
StrictModeUtils.detectAllThreadPolicy()
}
val prefs = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
val themeColor = prefs[themeColorKey]
val themeResource = getThemeResource(prefs, prefs[themeKey], themeColor)
val themeColor = themePreferences[themeColorKey]
val themeResource = getThemeResource(themePreferences, themePreferences[themeKey], themeColor)
if (themeResource != 0) {
setTheme(themeResource)
}
onApplyNavigationStyle(prefs[navbarStyleKey], themeColor)
onApplyNavigationStyle(themeNavigationStyle, themeColor)
super.onCreate(savedInstanceState)
ActivitySupport.setTaskDescription(this, TaskDescriptionCompat(title.toString(), null,
ColorUtils.setAlphaComponent(overrideTheme.colorToolbar, 0xFF)))

View File

@ -20,27 +20,177 @@
package org.mariotaku.twidere.activity
import android.accounts.AccountManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.support.annotation.StyleRes
import android.support.v4.view.ViewCompat
import android.support.v7.app.TwilightManagerAccessor
import android.view.View
import android.widget.Toast
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.activity_main.*
import nl.komponents.kovenant.Promise
import org.mariotaku.chameleon.Chameleon
import org.mariotaku.chameleon.ChameleonActivity
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.componentIcon
import org.mariotaku.ktextension.contains
import org.mariotaku.restfu.http.RestHttpClient
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.SHARED_PREFERENCES_NAME
import org.mariotaku.twidere.activity.iface.IBaseActivity
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_INTENT
import org.mariotaku.twidere.constant.themeColorKey
import org.mariotaku.twidere.constant.themeKey
import org.mariotaku.twidere.extension.model.hasInvalidAccount
import org.mariotaku.twidere.extension.model.shouldShow
import org.mariotaku.twidere.model.presentation.LaunchPresentation
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.task.filter.RefreshLaunchPresentationsTask
import org.mariotaku.twidere.util.DeviceUtils
import org.mariotaku.twidere.util.OnLinkClickHandler
import org.mariotaku.twidere.util.StrictModeUtils
import org.mariotaku.twidere.util.ThemeUtils
import org.mariotaku.twidere.util.cache.JsonCache
import org.mariotaku.twidere.util.dagger.GeneralComponent
import org.mariotaku.twidere.util.theme.getCurrentThemeResource
import javax.inject.Inject
open class MainActivity : BaseActivity() {
open class MainActivity : ChameleonActivity(), IBaseActivity<MainActivity> {
private val handler = Handler(Looper.getMainLooper())
private val launchLaterRunnable: Runnable = Runnable { launchMain() }
private val actionHelper = IBaseActivity.ActionHelper(this)
private var isNightBackup: Int = TwilightManagerAccessor.UNSPECIFIED
private val themePreferences by lazy {
getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
}
private val userTheme: Chameleon.Theme by lazy {
return@lazy ThemeUtils.getUserTheme(this, themePreferences)
}
@Inject
lateinit var restHttpClient: RestHttpClient
@Inject
lateinit var preferences: SharedPreferences
@Inject
lateinit var jsonCache: JsonCache
override fun onCreate(savedInstanceState: Bundle?) {
if (BuildConfig.DEBUG) {
StrictModeUtils.detectAllVmPolicy()
StrictModeUtils.detectAllThreadPolicy()
}
val themeColor = themePreferences[themeColorKey]
val themeResource = getThemeResource(themePreferences, themePreferences[themeKey], themeColor)
if (themeResource != 0) {
setTheme(themeResource)
}
super.onCreate(savedInstanceState)
GeneralComponent.get(this).inject(this)
setContentView(R.layout.activity_main)
main.visibility = View.VISIBLE
appIcon.setImageDrawable(componentIcon)
skipPresentation.setOnClickListener {
launchDirectly()
}
controlOverlay.setOnClickListener {
val presentation = controlOverlay.tag as? LaunchPresentation ?: return@setOnClickListener
val uri = presentation.url?.let(Uri::parse) ?: return@setOnClickListener
OnLinkClickHandler.openLink(this, preferences, uri)
}
ViewCompat.setOnApplyWindowInsetsListener(main) lambda@ { _, insets ->
main.setPadding(0, 0, 0, insets.systemWindowInsetBottom)
controlOverlay.setPadding(insets.systemWindowInsetLeft, insets.systemWindowInsetTop,
insets.systemWindowInsetRight, 0)
return@lambda insets.consumeSystemWindowInsets()
}
val presentation = jsonCache.getList(RefreshLaunchPresentationsTask.JSON_CACHE_KEY,
LaunchPresentation::class.java)?.firstOrNull {
it.shouldShow()
}
if (presentation != null) {
displayPresentation(presentation)
launchLater()
} else {
launchDirectly()
}
}
override fun onPause() {
actionHelper.dispatchOnPause()
super.onPause()
}
override fun onResume() {
super.onResume()
updateNightMode()
}
override fun onResumeFragments() {
super.onResumeFragments()
actionHelper.dispatchOnResumeFragments()
}
override fun executeAfterFragmentResumed(useHandler: Boolean, action: (MainActivity) -> Unit): Promise<Unit, Exception> {
return actionHelper.executeAfterFragmentResumed(useHandler, action)
}
override fun getOverrideTheme(): Chameleon.Theme? {
return userTheme
}
@StyleRes
protected open fun getThemeResource(preferences: SharedPreferences, theme: String, themeColor: Int): Int {
return getCurrentThemeResource(this, theme)
}
private fun updateNightMode() {
val nightState = TwilightManagerAccessor.getNightState(this)
if (isNightBackup != TwilightManagerAccessor.UNSPECIFIED && nightState != isNightBackup) {
recreate()
return
}
isNightBackup = nightState
}
private fun displayPresentation(presentation: LaunchPresentation) {
skipPresentation.visibility = View.VISIBLE
controlOverlay.tag = presentation
Glide.with(this).load(presentation.images.first().url).into(presentationView)
}
private fun launchDirectly() {
handler.removeCallbacks(launchLaterRunnable)
launchMain()
}
private fun launchLater() {
handler.postDelayed(launchLaterRunnable, 5000L)
}
private fun launchMain() {
if (isFinishing) return
executeAfterFragmentResumed { performLaunch() }
}
private fun performLaunch() {
val am = AccountManager.get(this)
if (!DeviceUtils.checkCompatibility()) {
startActivity(Intent(this, IncompatibleAlertActivity::class.java))

View File

@ -59,6 +59,7 @@ import org.mariotaku.twidere.service.StreamingService
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.content.TwidereSQLiteOpenHelper
import org.mariotaku.twidere.util.dagger.GeneralComponent
import org.mariotaku.twidere.util.emoji.EmojioneTranslator
import org.mariotaku.twidere.util.kovenant.startKovenant
import org.mariotaku.twidere.util.kovenant.stopKovenant
import org.mariotaku.twidere.util.media.MediaPreloader
@ -130,6 +131,7 @@ class TwidereApplication : Application(), Constants, OnSharedPreferenceChangeLis
StrictModeUtils.detectAllVmPolicy()
}
super.onCreate()
EmojioneTranslator.init(this)
applyLanguageSettings()
startKovenant()
initializeAsyncTask()

View File

@ -4,10 +4,10 @@ import android.content.ComponentName
import android.content.Context
import org.mariotaku.twidere.R
import org.mariotaku.twidere.model.FiltersSubscription
import org.mariotaku.twidere.model.filter.UrlFiltersSubscriptionProviderArguments
import org.mariotaku.twidere.util.JsonSerializer
import org.mariotaku.twidere.util.filter.FiltersSubscriptionProvider
import org.mariotaku.twidere.util.filter.LocalFiltersSubscriptionProvider
import org.mariotaku.twidere.util.filter.UrlFiltersSubscriptionProvider
/**
* Created by mariotaku on 2017/1/9.
@ -38,7 +38,7 @@ fun FiltersSubscription.getComponentLabel(context: Context): CharSequence {
fun FiltersSubscription.setupUrl(url: String) {
this.component = ":url"
this.arguments = JsonSerializer.serialize(UrlFiltersSubscriptionProvider.Arguments().apply {
this.arguments = JsonSerializer.serialize(UrlFiltersSubscriptionProviderArguments().apply {
this.url = url
})
}

View File

@ -0,0 +1,56 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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.mariotaku.twidere.extension.model
import org.mariotaku.twidere.model.presentation.LaunchPresentation
import java.util.*
/**
* Created by mariotaku on 2017/8/20.
*/
fun LaunchPresentation.shouldShow(): Boolean {
// Check language
val locale = Locale.getDefault()
if (locales != null && locales.none { it.matches(locale) }) {
return false
}
// Check date/time
val date = Date()
if (schedule != null && !schedule.matches(date)) {
return false
}
return true
}
fun LaunchPresentation.Schedule.matches(date: Date): Boolean {
val cal = Calendar.getInstance()
cal.time = date
return cron.matches(cal)
}
fun LaunchPresentation.Locale.matches(locale: Locale): Boolean {
if (language != locale.language) return false
if (country == null) {
return locale.country.isNullOrEmpty()
}
return country == locale.country
}

View File

@ -85,6 +85,7 @@ class JobTaskService : JobService() {
const val JOB_ID_REFRESH_NOTIFICATIONS = 2
const val JOB_ID_REFRESH_DIRECT_MESSAGES = 3
const val JOB_ID_REFRESH_FILTERS_SUBSCRIPTIONS = 19
const val JOB_ID_REFRESH_LAUNCH_PRESENTATIONS = 18
const val JOB_ID_SYNC_DRAFTS = 21
const val JOB_ID_SYNC_FILTERS = 22
const val JOB_ID_SYNC_USER_NICKNAMES = 23
@ -108,6 +109,7 @@ class JobTaskService : JobService() {
JOB_ID_REFRESH_NOTIFICATIONS -> TaskServiceRunner.ACTION_REFRESH_NOTIFICATIONS
JOB_ID_REFRESH_DIRECT_MESSAGES -> TaskServiceRunner.ACTION_REFRESH_DIRECT_MESSAGES
JOB_ID_REFRESH_FILTERS_SUBSCRIPTIONS -> TaskServiceRunner.ACTION_REFRESH_FILTERS_SUBSCRIPTIONS
JOB_ID_REFRESH_LAUNCH_PRESENTATIONS -> TaskServiceRunner.ACTION_REFRESH_LAUNCH_PRESENTATIONS
JOB_ID_SYNC_DRAFTS -> TaskServiceRunner.ACTION_SYNC_DRAFTS
JOB_ID_SYNC_FILTERS -> TaskServiceRunner.ACTION_SYNC_FILTERS
JOB_ID_SYNC_USER_NICKNAMES -> TaskServiceRunner.ACTION_SYNC_USER_NICKNAMES

View File

@ -6,11 +6,13 @@ import com.squareup.otto.Bus
import com.twitter.Extractor
import org.mariotaku.abstask.library.AbstractTask
import org.mariotaku.kpreferences.KPreferences
import org.mariotaku.restfu.http.RestHttpClient
import org.mariotaku.twidere.model.DefaultFeatures
import org.mariotaku.twidere.util.AsyncTwitterWrapper
import org.mariotaku.twidere.util.ErrorInfoStore
import org.mariotaku.twidere.util.ReadStateManager
import org.mariotaku.twidere.util.UserColorNameManager
import org.mariotaku.twidere.util.cache.JsonCache
import org.mariotaku.twidere.util.dagger.GeneralComponent
import org.mariotaku.twidere.util.media.MediaPreloader
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
@ -46,6 +48,8 @@ abstract class BaseAbstractTask<Params, Result, Callback>(val context: Context)
@Inject
lateinit var extraFeaturesService: ExtraFeaturesService
@Inject
lateinit var restHttpClient: RestHttpClient
@Inject
lateinit var defaultFeatures: DefaultFeatures
@Inject
lateinit var scheduleProviderFactory: StatusScheduleProvider.Factory
@ -55,6 +59,8 @@ abstract class BaseAbstractTask<Params, Result, Callback>(val context: Context)
lateinit var syncPreferences: SyncPreferences
@Inject
lateinit var timelineSyncManagerFactory: TimelineSyncManager.Factory
@Inject
lateinit var jsonCache: JsonCache
val scheduleProvider: StatusScheduleProvider?
get() = scheduleProviderFactory.newInstance(context)

View File

@ -0,0 +1,54 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program 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.
*
* This program 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.mariotaku.twidere.task.filter
import android.content.Context
import org.mariotaku.restfu.annotation.method.GET
import org.mariotaku.restfu.http.HttpRequest
import org.mariotaku.twidere.model.presentation.LaunchPresentation
import org.mariotaku.twidere.task.BaseAbstractTask
import org.mariotaku.twidere.util.JsonSerializer
import java.io.IOException
/**
* Created by mariotaku on 2017/8/22.
*/
class RefreshLaunchPresentationsTask(context: Context) : BaseAbstractTask<Unit?, Boolean, (Boolean) -> Unit>(context) {
override fun doLongOperation(params: Unit?): Boolean {
val request = HttpRequest.Builder()
.method(GET.METHOD)
.url("https://twidere.mariotaku.org/assets/data/launch_presentations.json")
.build()
try {
val presentations = restHttpClient.newCall(request).execute().use {
return@use JsonSerializer.parseList(it.body.stream(), LaunchPresentation::class.java)
}
jsonCache.saveList(JSON_CACHE_KEY, presentations, LaunchPresentation::class.java)
return true
} catch (e: IOException) {
return false
}
}
companion object {
const val JSON_CACHE_KEY = "launch_presentations"
}
}

View File

@ -22,6 +22,7 @@ import org.mariotaku.twidere.model.pagination.SinceMaxPagination
import org.mariotaku.twidere.provider.TwidereDataStore.Activities
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.filter.RefreshFiltersSubscriptionsTask
import org.mariotaku.twidere.task.filter.RefreshLaunchPresentationsTask
import org.mariotaku.twidere.task.twitter.GetActivitiesAboutMeTask
import org.mariotaku.twidere.task.twitter.GetHomeTimelineTask
import org.mariotaku.twidere.task.twitter.message.GetMessagesTask
@ -41,7 +42,8 @@ class TaskServiceRunner(
Log.d(LOGTAG, "TaskServiceRunner run task $action")
when (action) {
ACTION_REFRESH_HOME_TIMELINE, ACTION_REFRESH_NOTIFICATIONS,
ACTION_REFRESH_DIRECT_MESSAGES, ACTION_REFRESH_FILTERS_SUBSCRIPTIONS -> {
ACTION_REFRESH_DIRECT_MESSAGES, ACTION_REFRESH_FILTERS_SUBSCRIPTIONS,
ACTION_REFRESH_LAUNCH_PRESENTATIONS -> {
val task = createRefreshTask(action) ?: return false
task.callback = callback
TaskStarter.execute(task)
@ -97,13 +99,16 @@ class TaskServiceRunner(
ACTION_REFRESH_FILTERS_SUBSCRIPTIONS -> {
return RefreshFiltersSubscriptionsTask(context)
}
ACTION_REFRESH_LAUNCH_PRESENTATIONS -> {
return RefreshLaunchPresentationsTask(context)
}
}
return null
}
@StringDef(ACTION_REFRESH_HOME_TIMELINE, ACTION_REFRESH_NOTIFICATIONS, ACTION_REFRESH_DIRECT_MESSAGES,
ACTION_REFRESH_FILTERS_SUBSCRIPTIONS, ACTION_SYNC_DRAFTS, ACTION_SYNC_FILTERS,
ACTION_SYNC_USER_NICKNAMES, ACTION_SYNC_USER_COLORS)
ACTION_REFRESH_FILTERS_SUBSCRIPTIONS, ACTION_REFRESH_LAUNCH_PRESENTATIONS,
ACTION_SYNC_DRAFTS, ACTION_SYNC_FILTERS, ACTION_SYNC_USER_NICKNAMES, ACTION_SYNC_USER_COLORS)
@Retention(AnnotationRetention.SOURCE)
annotation class Action
@ -139,6 +144,8 @@ class TaskServiceRunner(
@Action
const val ACTION_REFRESH_FILTERS_SUBSCRIPTIONS = INTENT_PACKAGE_PREFIX + "REFRESH_FILTERS_SUBSCRIPTIONS"
@Action
const val ACTION_REFRESH_LAUNCH_PRESENTATIONS = INTENT_PACKAGE_PREFIX + "REFRESH_LAUNCH_PRESENTATIONS"
@Action
const val ACTION_SYNC_DRAFTS = INTENT_PACKAGE_PREFIX + "SYNC_DRAFTS"
@Action
const val ACTION_SYNC_FILTERS = INTENT_PACKAGE_PREFIX + "SYNC_FILTERS"

View File

@ -21,6 +21,7 @@ package org.mariotaku.twidere.util.cache
import com.bumptech.glide.disklrucache.DiskLruCache
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.util.DebugLog
import org.mariotaku.twidere.util.JsonSerializer
import java.io.File
import java.io.IOException
@ -29,22 +30,24 @@ import java.io.IOException
* Created by mariotaku on 2017/3/1.
*/
class JsonCache(val cacheDir: File) {
class JsonCache(cacheDir: File) {
private val cache = try {
private val cache: DiskLruCache? = try {
DiskLruCache.open(cacheDir, BuildConfig.VERSION_CODE, 1, 100 * 1048576)
} catch (e: IOException) {
DebugLog.w(tr = e)
null
}
fun <T> getList(key: String, cls: Class<T>): List<T>? {
val value = cache?.get(key) ?: return null
try {
return value.getFile(0)?.inputStream()?.use {
return try {
value.getFile(0)?.inputStream()?.use {
JsonSerializer.parseList(it, cls)
}
} catch (e: IOException) {
return null
DebugLog.w(tr = e)
null
}
}
@ -55,6 +58,8 @@ class JsonCache(val cacheDir: File) {
JsonSerializer.serialize(list, it, cls)
}
editor.commit()
} catch (e: IOException) {
DebugLog.w(tr = e)
} finally {
editor.abortUnlessCommitted()
}

View File

@ -22,10 +22,7 @@ package org.mariotaku.twidere.util.dagger
import android.content.Context
import android.support.v7.widget.RecyclerView
import dagger.Component
import org.mariotaku.twidere.activity.BaseActivity
import org.mariotaku.twidere.activity.ComposeActivity
import org.mariotaku.twidere.activity.MediaViewerActivity
import org.mariotaku.twidere.activity.PremiumDashboardActivity
import org.mariotaku.twidere.activity.*
import org.mariotaku.twidere.adapter.*
import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.fragment.BaseDialogFragment
@ -150,9 +147,11 @@ interface GeneralComponent {
fun inject(service: BaseService)
companion object {
fun inject(activity: MainActivity)
companion object {
private var instance: GeneralComponent? = null
fun get(context: Context): GeneralComponent {
return instance ?: run {
val helper = DaggerGeneralComponent.builder().applicationModule(ApplicationModule.get(context)).build()

View File

@ -19,9 +19,26 @@
package org.mariotaku.twidere.util.emoji
import android.content.Context
import org.apache.commons.text.translate.CharSequenceTranslator
import org.mariotaku.commons.emojione.ShortnameToUnicodeTranslator
import java.io.Writer
/**
* Created by mariotaku on 2017/4/26.
*/
object EmojioneTranslator: ShortnameToUnicodeTranslator()
object EmojioneTranslator: CharSequenceTranslator() {
private var implementation: ShortnameToUnicodeTranslator? = null
fun init(context: Context) {
if (implementation != null)return
implementation = ShortnameToUnicodeTranslator(context)
}
override fun translate(input: CharSequence?, index: Int, out: Writer?): Int {
val translator = implementation ?: return 0
return translator.translate(input, index, out)
}
}

View File

@ -1,6 +1,7 @@
package org.mariotaku.twidere.util.filter
import android.content.Context
import org.mariotaku.twidere.model.filter.UrlFiltersSubscriptionProviderArguments
import org.mariotaku.twidere.util.JsonSerializer
import java.io.IOException
@ -15,7 +16,7 @@ abstract class LocalFiltersSubscriptionProvider(val context: Context) : FiltersS
"url" -> {
if (arguments == null) return null
val argsObj = try {
JsonSerializer.parse(arguments, UrlFiltersSubscriptionProvider.Arguments::class.java)
JsonSerializer.parse(arguments, UrlFiltersSubscriptionProviderArguments::class.java)
} catch (e: IOException) {
return null
}

View File

@ -2,8 +2,6 @@ package org.mariotaku.twidere.util.filter
import android.content.Context
import android.net.Uri
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import org.mariotaku.restfu.annotation.method.GET
import org.mariotaku.restfu.http.HttpRequest
import org.mariotaku.restfu.http.MultiValueMap
@ -12,6 +10,7 @@ import org.mariotaku.restfu.http.mime.Body
import org.mariotaku.twidere.extension.model.parse
import org.mariotaku.twidere.extension.newPullParser
import org.mariotaku.twidere.model.FiltersData
import org.mariotaku.twidere.model.filter.UrlFiltersSubscriptionProviderArguments
import org.mariotaku.twidere.util.ETagCache
import org.mariotaku.twidere.util.JsonSerializer
import org.mariotaku.twidere.util.dagger.GeneralComponent
@ -22,7 +21,10 @@ import javax.inject.Inject
* Created by mariotaku on 2017/1/9.
*/
class UrlFiltersSubscriptionProvider(context: Context, val arguments: Arguments) : LocalFiltersSubscriptionProvider(context) {
class UrlFiltersSubscriptionProvider(
context: Context,
val arguments: UrlFiltersSubscriptionProviderArguments
) : LocalFiltersSubscriptionProvider(context) {
@Inject
internal lateinit var restHttpClient: RestHttpClient
@Inject
@ -110,9 +112,4 @@ class UrlFiltersSubscriptionProvider(context: Context, val arguments: Arguments)
}
}
@JsonObject
class Arguments {
@JsonField(name = arrayOf("url"))
lateinit var url: String
}
}

View File

@ -11,6 +11,7 @@ import org.mariotaku.twidere.annotation.AutoRefreshType
import org.mariotaku.twidere.constant.refreshIntervalKey
import org.mariotaku.twidere.service.JobTaskService
import org.mariotaku.twidere.service.JobTaskService.Companion.JOB_ID_REFRESH_FILTERS_SUBSCRIPTIONS
import org.mariotaku.twidere.service.JobTaskService.Companion.JOB_ID_REFRESH_LAUNCH_PRESENTATIONS
import java.util.concurrent.TimeUnit
import android.Manifest.permission as AndroidPermissions
@ -23,7 +24,7 @@ class JobSchedulerAutoRefreshController(
context: Context,
kPreferences: KPreferences
) : AutoRefreshController(context, kPreferences) {
val scheduler: JobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
private val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
override fun appStarted() {
val allJobs = scheduler.allPendingJobs
@ -37,6 +38,9 @@ class JobSchedulerAutoRefreshController(
if (allJobs.none { job -> job.id == JOB_ID_REFRESH_FILTERS_SUBSCRIPTIONS }) {
scheduleJob(JOB_ID_REFRESH_FILTERS_SUBSCRIPTIONS, TimeUnit.HOURS.toMillis(4))
}
if (allJobs.none { job -> job.id == JOB_ID_REFRESH_LAUNCH_PRESENTATIONS }) {
scheduleJob(JOB_ID_REFRESH_LAUNCH_PRESENTATIONS, TimeUnit.HOURS.toMillis(6))
}
}
override fun schedule(@AutoRefreshType type: String) {

View File

@ -15,6 +15,7 @@ import org.mariotaku.twidere.constant.refreshIntervalKey
import org.mariotaku.twidere.service.JobTaskService.Companion.JOB_IDS_REFRESH
import org.mariotaku.twidere.service.LegacyTaskService
import org.mariotaku.twidere.util.TaskServiceRunner.Companion.ACTION_REFRESH_FILTERS_SUBSCRIPTIONS
import org.mariotaku.twidere.util.TaskServiceRunner.Companion.ACTION_REFRESH_LAUNCH_PRESENTATIONS
import java.util.concurrent.TimeUnit
class LegacyAutoRefreshController(
@ -37,6 +38,7 @@ class LegacyAutoRefreshController(
override fun appStarted() {
rescheduleAll()
rescheduleFiltersSubscriptionsRefresh()
rescheduleLaunchPresentationsRefresh()
}
override fun rescheduleAll() {
@ -69,6 +71,15 @@ class LegacyAutoRefreshController(
PendingIntent.getService(context, 0, intent, 0))
}
private fun rescheduleLaunchPresentationsRefresh() {
val interval = TimeUnit.HOURS.toMillis(6)
val triggerAt = SystemClock.elapsedRealtime() + interval
val intent = Intent(context, LegacyTaskService::class.java)
intent.action = ACTION_REFRESH_LAUNCH_PRESENTATIONS
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAt, interval,
PendingIntent.getService(context, 0, intent, 0))
}
companion object {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun removeAllJobs(context: Context, jobIds: IntArray) {

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program 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.
~
~ This program 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/>.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/presentationView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/logoContainer"
android:scaleType="centerCrop"/>
<LinearLayout
android:id="@+id/logoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:minHeight="108dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/appIcon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@mipmap/ic_launcher"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/element_spacing_normal"
android:layout_marginStart="@dimen/element_spacing_normal"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="?android:textAppearanceMedium"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_description"
android:textAppearance="?android:textAppearanceSmall"/>
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/controlOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/skipPresentation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="@dimen/element_spacing_normal"
android:layout_marginRight="@dimen/element_spacing_normal"
android:layout_marginTop="@dimen/element_spacing_normal"
android:text="@string/action_skip"
android:visibility="gone"/>
</FrameLayout>
</RelativeLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program 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.
~
~ This program 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/>.
-->
<resources>
<style name="Theme.Twidere.Launcher" parent="Theme.Twidere.Dark.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<item name="darkThemeResource">@style/Theme.Twidere.Dark.Launcher</item>
<item name="lightThemeResource">@style/Theme.Twidere.Light.Launcher</item>
</style>
</resources>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program 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.
~
~ This program 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/>.
-->
<resources>
<style name="Theme.Twidere.Dark.Launcher" parent="Theme.Twidere.Dark.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
</resources>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program 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.
~
~ This program 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/>.
-->
<resources>
<style name="Theme.Twidere.Light.Launcher" parent="Theme.Twidere.Light.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
</resources>

View File

@ -57,4 +57,9 @@
<item name="colorPrimary">@color/nyan_background</item>
</style>
<style name="Theme.Twidere.Launcher" parent="Theme.Twidere.Dark.NoActionBar">
<item name="darkThemeResource">@style/Theme.Twidere.Dark.Launcher</item>
<item name="lightThemeResource">@style/Theme.Twidere.Light.Launcher</item>
</style>
</resources>

View File

@ -78,7 +78,6 @@
<style name="Theme.Twidere.Dark.NoActionBar" parent="Theme.Twidere.Dark">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.Twidere.Dark.ActionBar" parent="Theme.Twidere.Dark">
@ -119,4 +118,6 @@
<item name="android:windowActionModeOverlay">false</item>
</style>
<style name="Theme.Twidere.Dark.Launcher" parent="Theme.Twidere.Dark.NoActionBar"/>
</resources>

View File

@ -125,4 +125,7 @@
<item name="android:windowActionBarOverlay">false</item>
<item name="android:windowActionModeOverlay">false</item>
</style>
<style name="Theme.Twidere.Light.Launcher" parent="Theme.Twidere.Light.NoActionBar"/>
</resources>