diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml
index 2c5cffa7..07125a22 100644
--- a/.idea/dictionaries/tateisu.xml
+++ b/.idea/dictionaries/tateisu.xml
@@ -68,6 +68,7 @@
ihdr
infos
iptc
+ jetbrains
jfif
jwcp
kapt
diff --git a/app/build.gradle b/app/build.gradle
index f37bd3c9..154983f6 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -105,9 +105,10 @@ dependencies {
// PreferenceManager
implementation "androidx.preference:preference-ktx:1.1.1"
- implementation "androidx.exifinterface:exifinterface:1.3.1"
+ implementation "androidx.exifinterface:exifinterface:1.3.2"
// CustomTabs
+ // TODO 1.3.0 に上げたいが、その前に2回開く問題を調査したい
implementation "androidx.browser:browser:1.2.0"
// Recyclerview
@@ -123,7 +124,8 @@ dependencies {
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_coroutines_version"
+ implementation("ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0")
// Anko Layouts
// sdk15, sdk19, sdk21, sdk23 are also available
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt
index 918af0d1..01bf8ddd 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt
@@ -35,6 +35,7 @@ import jp.juggler.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
@@ -1580,7 +1581,9 @@ class ActAccountSetting : AsyncActivity(), View.OnClickListener,
)
override fun background(client : TootApiClient) : TootApiResult? {
- return wps.updateSubscription(client, true)
+ return runBlocking{
+ wps.updateSubscription(client, true)
+ }
}
override fun handleResult(result : TootApiResult?) {
diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.kt b/app/src/main/java/jp/juggler/subwaytooter/Column.kt
index afb947ae..37e0d375 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/Column.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/Column.kt
@@ -538,7 +538,11 @@ class Column(
// プロフカラムでのアカウント情報
@Volatile
internal var who_account : TootAccountRef? = null
-
+
+ // プロフカラムでのfeatured tag 情報(Mastodon3.3.0)
+ @Volatile
+ internal var who_featured_tags : List? = null
+
// リストカラムでのリスト情報
@Volatile
internal var list_info : TootList? = null
@@ -1733,8 +1737,14 @@ class Column(
client.request(String.format(Locale.JAPAN, PATH_ACCOUNT, profile_id))?.also { result1 ->
TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a ->
this.who_account = a
+
+ this.who_featured_tags = null
+ client.request("/api/v1/accounts/${profile_id}/featured_tags")?.also{ result2->
+
+ this.who_featured_tags =TootTag.parseListOrNull(parser,result2.jsonArray)
+ }
+
client.publishApiProgress("") // カラムヘッダの再表示
-
}
}
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt
index 617889fd..48c0d2e8 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt
@@ -21,7 +21,7 @@ import android.os.SystemClock
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
-import com.google.firebase.iid.FirebaseInstanceId
+import com.google.firebase.messaging.FirebaseMessaging
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
@@ -31,8 +31,11 @@ import jp.juggler.subwaytooter.table.NotificationCache.Companion.getEntityOrderI
import jp.juggler.subwaytooter.table.NotificationCache.Companion.parseNotificationType
import jp.juggler.subwaytooter.util.*
import jp.juggler.util.*
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.tasks.await
import okhttp3.Call
import okhttp3.Request
+import ru.gildor.coroutines.okhttp.await
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
@@ -41,931 +44,923 @@ import java.util.concurrent.atomic.AtomicReference
import kotlin.math.max
import kotlin.math.min
-class PollingWorker private constructor(contextArg : Context) {
-
- interface JobStatusCallback {
-
- fun onStatus(sv : String)
- }
-
- enum class TrackingType(val str : String) {
- All("all"),
- Reply("reply"),
- NotReply("notReply");
-
- companion object {
-
- fun parseStr(str : String?) : TrackingType {
- for(v in values()) {
- if(v.str == str) return v
- }
- return All
- }
- }
-
- }
-
- companion object {
-
- internal val log = LogCategory("PollingWorker")
-
- private const val FCM_SENDER_ID = "433682361381"
- private const val FCM_SCOPE = "FCM"
-
- const val NOTIFICATION_ID = 1
- const val NOTIFICATION_ID_ERROR = 3
-
- val mBusyAppDataImportBefore = AtomicBoolean(false)
- val mBusyAppDataImportAfter = AtomicBoolean(false)
-
- const val EXTRA_DB_ID = "db_id"
- const val EXTRA_TAG = "tag"
- const val EXTRA_TASK_ID = "task_id"
- const val EXTRA_NOTIFICATION_TYPE = "notification_type"
- const val EXTRA_NOTIFICATION_ID = "notificationId"
-
- const val APP_SERVER = "https://mastodon-msg.juggler.jp"
-
- internal val inject_queue = ConcurrentLinkedQueue()
-
- // ジョブID
- const val JOB_POLLING = 1
- private const val JOB_TASK = 2
- const val JOB_FCM = 3
-
- // タスクID
- const val TASK_POLLING = 1
- const val TASK_DATA_INJECTED = 2
- const val TASK_NOTIFICATION_CLEAR = 3
- const val TASK_APP_DATA_IMPORT_BEFORE = 4
- const val TASK_APP_DATA_IMPORT_AFTER = 5
- const val TASK_FCM_DEVICE_TOKEN = 6
- const val TASK_FCM_MESSAGE = 7
- const val TASK_BOOT_COMPLETED = 8
- const val TASK_PACKAGE_REPLACED = 9
- const val TASK_NOTIFICATION_DELETE = 10
- const val TASK_NOTIFICATION_CLICK = 11
- private const val TASK_UPDATE_NOTIFICATION = 12
-
- @SuppressLint("StaticFieldLeak")
- private var sInstance : PollingWorker? = null
-
- fun getInstance(applicationContext : Context) : PollingWorker {
- var s = sInstance
- if(s == null) {
- s = PollingWorker(applicationContext)
- sInstance = s
- }
- return s
- }
-
- fun getDeviceId(context : Context) : String? {
- // 設定ファイルに保持されていたらそれを使う
+class PollingWorker private constructor(contextArg: Context) {
+
+ interface JobStatusCallback {
+
+ fun onStatus(sv: String)
+ }
+
+ enum class TrackingType(val str: String) {
+ All("all"),
+ Reply("reply"),
+ NotReply("notReply");
+
+ companion object {
+
+ fun parseStr(str: String?): TrackingType {
+ for (v in values()) {
+ if (v.str == str) return v
+ }
+ return All
+ }
+ }
+
+ }
+
+ companion object {
+
+ internal val log = LogCategory("PollingWorker")
+
+ // private const val FCM_SENDER_ID = "433682361381"
+ // private const val FCM_SCOPE = "FCM"
+
+ const val NOTIFICATION_ID = 1
+ const val NOTIFICATION_ID_ERROR = 3
+
+ val mBusyAppDataImportBefore = AtomicBoolean(false)
+ val mBusyAppDataImportAfter = AtomicBoolean(false)
+
+ const val EXTRA_DB_ID = "db_id"
+ const val EXTRA_TAG = "tag"
+ const val EXTRA_TASK_ID = "task_id"
+ const val EXTRA_NOTIFICATION_TYPE = "notification_type"
+ const val EXTRA_NOTIFICATION_ID = "notificationId"
+
+ const val APP_SERVER = "https://mastodon-msg.juggler.jp"
+
+ internal val inject_queue = ConcurrentLinkedQueue()
+
+ // ジョブID
+ const val JOB_POLLING = 1
+ private const val JOB_TASK = 2
+ const val JOB_FCM = 3
+
+ // タスクID
+ const val TASK_POLLING = 1
+ const val TASK_DATA_INJECTED = 2
+ const val TASK_NOTIFICATION_CLEAR = 3
+ const val TASK_APP_DATA_IMPORT_BEFORE = 4
+ const val TASK_APP_DATA_IMPORT_AFTER = 5
+ const val TASK_FCM_DEVICE_TOKEN = 6
+ const val TASK_FCM_MESSAGE = 7
+ const val TASK_BOOT_COMPLETED = 8
+ const val TASK_PACKAGE_REPLACED = 9
+ const val TASK_NOTIFICATION_DELETE = 10
+ const val TASK_NOTIFICATION_CLICK = 11
+ private const val TASK_UPDATE_NOTIFICATION = 12
+
+ @SuppressLint("StaticFieldLeak")
+ private var sInstance: PollingWorker? = null
+
+ fun getInstance(applicationContext: Context): PollingWorker {
+ var s = sInstance
+ if (s == null) {
+ s = PollingWorker(applicationContext)
+ sInstance = s
+ }
+ return s
+ }
+
+ suspend fun getFirebaseMessagingToken(context:Context):String?{
val prefDevice = PrefDevice.prefDevice(context)
- var device_token = prefDevice.getString(PrefDevice.KEY_DEVICE_TOKEN, null)
- if(device_token?.isNotEmpty() == true) return device_token
-
- try {
- // FirebaseのAPIから取得する
- device_token = FirebaseInstanceId.getInstance().getToken(FCM_SENDER_ID, FCM_SCOPE)
- if(device_token?.isNotEmpty() == true) {
+ // 設定ファイルに保持されていたらそれを使う
+ prefDevice
+ .getString(PrefDevice.KEY_DEVICE_TOKEN, null)
+ ?.notEmpty()?.let{ return it}
+
+ // 古い形式
+ // return FirebaseInstanceId.getInstance().getToken(FCM_SENDER_ID, FCM_SCOPE)
+
+ // com.google.firebase:firebase-messaging.20.3.0 以降
+ // implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_coroutines_version"
+ try{
+ val sv = FirebaseMessaging.getInstance().token.await()
+ return if (sv.isNullOrBlank()) {
+ log.e("getFirebaseMessagingToken: missing device token.")
+ null
+ } else {
prefDevice
.edit()
- .putString(PrefDevice.KEY_DEVICE_TOKEN, device_token)
+ .putString(PrefDevice.KEY_DEVICE_TOKEN, sv)
.apply()
- return device_token
+ sv
}
- log.e("getDeviceId: missing device token.")
- return null
- } catch(ex : Throwable) {
- log.trace(ex, "getDeviceId: could not get device token.")
+ }catch(ex:Throwable){
+ log.trace(ex, "getFirebaseMessagingToken: could not get device token.")
return null
}
}
-
- // インストールIDを生成する前に、各データの通知登録キャッシュをクリアする
- // トークンがまだ生成されていない場合、このメソッドは null を返します。
- fun prepareInstallId(
- context : Context,
- job : JobItem? = null
- ) : String? {
- val prefDevice = PrefDevice.prefDevice(context)
-
- var sv = prefDevice.getString(PrefDevice.KEY_INSTALL_ID, null)
- if(sv?.isNotEmpty() == true) return sv
-
- SavedAccount.clearRegistrationCache()
-
- try {
- var device_token = prefDevice.getString(PrefDevice.KEY_DEVICE_TOKEN, null)
- if(device_token?.isEmpty() != false) {
- try {
- device_token =
- FirebaseInstanceId.getInstance().getToken(FCM_SENDER_ID, FCM_SCOPE)
- if(device_token == null || device_token.isEmpty()) {
- log.e("getInstallId: missing device token.")
- return null
- } else {
- prefDevice.edit().putString(PrefDevice.KEY_DEVICE_TOKEN, device_token)
- .apply()
- }
- } catch(ex : Throwable) {
- log.trace(ex, "getInstallId: could not get device token.")
- return null
- }
- }
-
- val request = Request.Builder()
- .url("$APP_SERVER/counter")
- .build()
-
- val call = App1.ok_http_client.newCall(request)
- if(job != null) {
- job.current_call = call
- }
-
- val response = call.execute()
- val body = response.body?.string()
-
- if(! response.isSuccessful || body?.isEmpty() != false) {
- log.e(
+
+
+
+ // インストールIDを生成する前に、各データの通知登録キャッシュをクリアする
+ // トークンがまだ生成されていない場合、このメソッドは null を返します。
+ @Suppress("BlockingMethodInNonBlockingContext")
+ suspend fun prepareInstallId(
+ context: Context,
+ job: JobItem? = null
+ ): String? {
+ val prefDevice = PrefDevice.prefDevice(context)
+
+ var sv = prefDevice.getString(PrefDevice.KEY_INSTALL_ID, null)
+ if (sv?.isNotEmpty() == true) return sv
+
+ SavedAccount.clearRegistrationCache()
+
+ try {
+ val device_token = getFirebaseMessagingToken(context)
+ ?: return null
+
+ val request = Request.Builder()
+ .url("$APP_SERVER/counter")
+ .build()
+
+ val call = App1.ok_http_client.newCall(request)
+ if (job != null) job.current_call = call
+ val response = call.await()
+
+ val body = response.body?.string()
+
+ if (!response.isSuccessful || body?.isEmpty() != false) {
+ log.e(
TootApiClient.formatResponse(
response,
"getInstallId: get /counter failed."
)
)
- return null
- }
-
- sv = (device_token + UUID.randomUUID() + body).digestSHA256Base64Url()
- prefDevice.edit().putString(PrefDevice.KEY_INSTALL_ID, sv).apply()
-
- return sv
-
- } catch(ex : Throwable) {
- log.trace(ex, "prepareInstallId failed.")
- }
- return null
- }
-
- //////////////////////////////////////////////////////////////////////
- // タスクの管理
-
- val task_list = TaskList()
-
- fun scheduleJob(context : Context, job_id : Int) {
-
- val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE)
- as? JobScheduler
- ?: throw NotImplementedError("missing JobScheduler system service")
-
- val component = ComponentName(context, PollingService::class.java)
-
- val builder = JobInfo.Builder(job_id, component)
- .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
-
- if(job_id == JOB_POLLING) {
-
- val minute = 60000L
-
- val intervalMillis = max(
+ return null
+ }
+
+ sv = (device_token + UUID.randomUUID() + body).digestSHA256Base64Url()
+ prefDevice.edit().putString(PrefDevice.KEY_INSTALL_ID, sv).apply()
+
+ return sv
+
+ } catch (ex: Throwable) {
+ log.trace(ex, "prepareInstallId failed.")
+ }
+ return null
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // タスクの管理
+
+ val task_list = TaskList()
+
+ fun scheduleJob(context: Context, job_id: Int) {
+
+ val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE)
+ as? JobScheduler
+ ?: throw NotImplementedError("missing JobScheduler system service")
+
+ val component = ComponentName(context, PollingService::class.java)
+
+ val builder = JobInfo.Builder(job_id, component)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+
+ if (job_id == JOB_POLLING) {
+
+ val minute = 60000L
+
+ val intervalMillis = max(
minute * 5L,
minute * Pref.spPullNotificationCheckInterval.toInt(context.pref())
)
-
- val flexMillis = max(
+
+ val flexMillis = max(
minute,
intervalMillis shr 1
)
-
- fun JobInfo.Builder.setPeriodicCompat(intervalMillis : Long, flexMillis : Long) =
- this.apply {
- if(Build.VERSION.SDK_INT >= 24) {
- builder.setPeriodic(intervalMillis, flexMillis)
- } else {
- builder.setPeriodic(intervalMillis)
- }
- }
-
- builder
- .setPeriodicCompat(intervalMillis, flexMillis)
- .setPersisted(true)
-
- } else {
- builder
- .setMinimumLatency(0)
- .setOverrideDeadline(60000L)
- }
- val jobInfo = builder.build()
-
- val rv = scheduler.schedule(jobInfo)
- if(rv != JobScheduler.RESULT_SUCCESS) {
- log.w("scheduler.schedule failed. rv=$rv")
- }
- }
-
- // タスクの追加
- private fun addTask(
- context : Context,
- removeOld : Boolean,
- task_id : Int,
- taskDataArg : JsonObject?
+
+ fun JobInfo.Builder.setPeriodicCompat(intervalMillis: Long, flexMillis: Long) =
+ this.apply {
+ if (Build.VERSION.SDK_INT >= 24) {
+ builder.setPeriodic(intervalMillis, flexMillis)
+ } else {
+ builder.setPeriodic(intervalMillis)
+ }
+ }
+
+ builder
+ .setPeriodicCompat(intervalMillis, flexMillis)
+ .setPersisted(true)
+
+ } else {
+ builder
+ .setMinimumLatency(0)
+ .setOverrideDeadline(60000L)
+ }
+ val jobInfo = builder.build()
+
+ val rv = scheduler.schedule(jobInfo)
+ if (rv != JobScheduler.RESULT_SUCCESS) {
+ log.w("scheduler.schedule failed. rv=$rv")
+ }
+ }
+
+ // タスクの追加
+ private fun addTask(
+ context: Context,
+ removeOld: Boolean,
+ task_id: Int,
+ taskDataArg: JsonObject?
) {
- try {
- task_list.addLast(
+ try {
+ task_list.addLast(
context,
removeOld,
(taskDataArg ?: JsonObject()).apply {
put(EXTRA_TASK_ID, task_id)
}
)
- scheduleJob(context, JOB_TASK)
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- }
-
- fun queueUpdateNotification(context : Context) {
- addTask(context, true, TASK_UPDATE_NOTIFICATION, null)
- }
-
- fun injectData(
- context : Context,
- account : SavedAccount,
- src : List
+ scheduleJob(context, JOB_TASK)
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ }
+
+ fun queueUpdateNotification(context: Context) {
+ addTask(context, true, TASK_UPDATE_NOTIFICATION, null)
+ }
+
+ fun injectData(
+ context: Context,
+ account: SavedAccount,
+ src: List
) {
-
- if(src.isEmpty()) return
-
- val id = InjectData()
- id.account_db_id = account.db_id
- id.list.addAll(src)
- inject_queue.add(id)
-
- addTask(context, true, TASK_DATA_INJECTED, null)
- }
-
- fun queueNotificationCleared(context : Context, db_id : Long) {
- try {
- val data = jsonObject {
- putNotNull(EXTRA_DB_ID, db_id)
- }
- addTask(context, true, TASK_NOTIFICATION_CLEAR, data)
- } catch(ex : JsonException) {
- log.trace(ex)
- }
-
- }
-
- private fun decodeNotificationUri(uri : Uri?) : JsonObject? {
- uri ?: return null
- return jsonObject {
- putNotNull(
+
+ if (src.isEmpty()) return
+
+ val id = InjectData()
+ id.account_db_id = account.db_id
+ id.list.addAll(src)
+ inject_queue.add(id)
+
+ addTask(context, true, TASK_DATA_INJECTED, null)
+ }
+
+ fun queueNotificationCleared(context: Context, db_id: Long) {
+ try {
+ val data = jsonObject {
+ putNotNull(EXTRA_DB_ID, db_id)
+ }
+ addTask(context, true, TASK_NOTIFICATION_CLEAR, data)
+ } catch (ex: JsonException) {
+ log.trace(ex)
+ }
+
+ }
+
+ private fun decodeNotificationUri(uri: Uri?): JsonObject? {
+ uri ?: return null
+ return jsonObject {
+ putNotNull(
EXTRA_DB_ID,
uri.getQueryParameter("db_id")?.toLongOrNull()
)
- putNotNull(
+ putNotNull(
EXTRA_NOTIFICATION_TYPE,
uri.getQueryParameter("type")?.notEmpty()
)
- putNotNull(
+ putNotNull(
EXTRA_NOTIFICATION_ID,
uri.getQueryParameter("notificationId")?.notEmpty()
)
- }
- }
-
- fun queueNotificationDeleted(context : Context, uri : Uri?) {
- try {
- val params = decodeNotificationUri(uri) ?: return
- addTask(context, false, TASK_NOTIFICATION_DELETE, params)
- } catch(ex : JsonException) {
- log.trace(ex)
- }
- }
-
- fun queueNotificationClicked(context : Context, uri : Uri?) {
- try {
- val params = decodeNotificationUri(uri) ?: return
- addTask(context, true, TASK_NOTIFICATION_CLICK, params)
- } catch(ex : JsonException) {
- log.trace(ex)
- }
- }
-
- fun queueAppDataImportBefore(context : Context) {
- mBusyAppDataImportBefore.set(true)
- mBusyAppDataImportAfter.set(true)
- addTask(context, false, TASK_APP_DATA_IMPORT_BEFORE, null)
- }
-
- fun queueAppDataImportAfter(context : Context) {
- addTask(context, false, TASK_APP_DATA_IMPORT_AFTER, null)
- }
-
- fun queueFCMTokenUpdated(context : Context) {
- addTask(context, true, TASK_FCM_DEVICE_TOKEN, null)
- }
-
- fun queueBootCompleted(context : Context) {
- addTask(context, true, TASK_BOOT_COMPLETED, null)
- }
-
- fun queuePackageReplaced(context : Context) {
- addTask(context, true, TASK_PACKAGE_REPLACED, null)
- }
-
- internal val job_status = AtomicReference(null)
-
- fun handleFCMMessage(context : Context, tag : String?, callback : JobStatusCallback) {
- log.d("handleFCMMessage: start. tag=$tag")
- val time_start = SystemClock.elapsedRealtime()
-
- callback.onStatus("=>")
-
- // タスクを追加
- val data = JsonObject().apply {
- try {
- putNotNull(EXTRA_TAG, tag)
- this[EXTRA_TASK_ID] = TASK_FCM_MESSAGE
- } catch(_ : JsonException) {
- }
- }
-
- task_list.addLast(context, true, data)
-
- callback.onStatus("==>")
-
- // 疑似ジョブを開始
- val pw = getInstance(context)
- pw.addJobFCM()
-
- // 疑似ジョブが終了するまで待機する
- while(true) {
- // ジョブが完了した?
- val now = SystemClock.elapsedRealtime()
- if(! pw.hasJob(JOB_FCM)) {
- log.d(
+ }
+ }
+
+ fun queueNotificationDeleted(context: Context, uri: Uri?) {
+ try {
+ val params = decodeNotificationUri(uri) ?: return
+ addTask(context, false, TASK_NOTIFICATION_DELETE, params)
+ } catch (ex: JsonException) {
+ log.trace(ex)
+ }
+ }
+
+ fun queueNotificationClicked(context: Context, uri: Uri?) {
+ try {
+ val params = decodeNotificationUri(uri) ?: return
+ addTask(context, true, TASK_NOTIFICATION_CLICK, params)
+ } catch (ex: JsonException) {
+ log.trace(ex)
+ }
+ }
+
+ fun queueAppDataImportBefore(context: Context) {
+ mBusyAppDataImportBefore.set(true)
+ mBusyAppDataImportAfter.set(true)
+ addTask(context, false, TASK_APP_DATA_IMPORT_BEFORE, null)
+ }
+
+ fun queueAppDataImportAfter(context: Context) {
+ addTask(context, false, TASK_APP_DATA_IMPORT_AFTER, null)
+ }
+
+ fun queueFCMTokenUpdated(context: Context) {
+ addTask(context, true, TASK_FCM_DEVICE_TOKEN, null)
+ }
+
+ fun queueBootCompleted(context: Context) {
+ addTask(context, true, TASK_BOOT_COMPLETED, null)
+ }
+
+ fun queuePackageReplaced(context: Context) {
+ addTask(context, true, TASK_PACKAGE_REPLACED, null)
+ }
+
+ internal val job_status = AtomicReference(null)
+
+ fun handleFCMMessage(context: Context, tag: String?, callback: JobStatusCallback) {
+ log.d("handleFCMMessage: start. tag=$tag")
+ val time_start = SystemClock.elapsedRealtime()
+
+ callback.onStatus("=>")
+
+ // タスクを追加
+ val data = JsonObject().apply {
+ try {
+ putNotNull(EXTRA_TAG, tag)
+ this[EXTRA_TASK_ID] = TASK_FCM_MESSAGE
+ } catch (_: JsonException) {
+ }
+ }
+
+ task_list.addLast(context, true, data)
+
+ callback.onStatus("==>")
+
+ // 疑似ジョブを開始
+ val pw = getInstance(context)
+ pw.addJobFCM()
+
+ // 疑似ジョブが終了するまで待機する
+ while (true) {
+ // ジョブが完了した?
+ val now = SystemClock.elapsedRealtime()
+ if (!pw.hasJob(JOB_FCM)) {
+ log.d(
"handleFCMMessage: JOB_FCM completed. time=%.2f",
(now - time_start) / 1000f
)
- break
- }
- // ジョブの状況を通知する
- var sv : String? = job_status.get()
- if(sv == null) sv = "(null)"
- callback.onStatus(sv)
-
- // 少し待機
- try {
- Thread.sleep(50L)
- } catch(ex : InterruptedException) {
- log.e(ex, "handleFCMMessage: blocking is interrupted.")
- break
- }
-
- }
- }
- }
-
- internal val context : Context
- private val appState : AppState
- internal val handler : Handler
- internal val pref : SharedPreferences
- private val connectivityManager : ConnectivityManager
- internal val notification_manager : NotificationManager
- internal val scheduler : JobScheduler
- private val power_manager : PowerManager?
- internal val power_lock : PowerManager.WakeLock
- private val wifi_manager : WifiManager?
- internal val wifi_lock : WifiManager.WifiLock
-
- private var worker : Worker
-
- internal val job_list = LinkedList()
-
- internal class Data(val access_info : SavedAccount, val notification : TootNotification)
-
- internal class InjectData {
-
- var account_db_id : Long = 0
- val list = ArrayList()
- }
-
- init {
- log.d("ctor")
-
- val context = contextArg.applicationContext
-
- this.context = context
-
- // クラッシュレポートによると App1.onCreate より前にここを通る場合がある
- // データベースへアクセスできるようにする
- this.appState = App1.prepare(context, "PollingWorker.ctor()")
- this.pref = App1.pref
- this.handler = appState.handler
-
- this.connectivityManager = systemService(context)
- ?: error("missing ConnectivityManager system service")
-
-
- this.notification_manager = systemService(context)
- ?: error("missing NotificationManager system service")
-
- this.scheduler = systemService(context)
- ?: error("missing JobScheduler system service")
-
- this.power_manager = systemService(context)
- ?: error("missing PowerManager system service")
-
- // WifiManagerの取得時はgetApplicationContext を使わないとlintに怒られる
- this.wifi_manager = systemService(context.applicationContext)
- ?: error("missing WifiManager system service")
-
- power_lock = power_manager.newWakeLock(
+ break
+ }
+ // ジョブの状況を通知する
+ var sv: String? = job_status.get()
+ if (sv == null) sv = "(null)"
+ callback.onStatus(sv)
+
+ // 少し待機
+ try {
+ Thread.sleep(50L)
+ } catch (ex: InterruptedException) {
+ log.e(ex, "handleFCMMessage: blocking is interrupted.")
+ break
+ }
+
+ }
+ }
+ }
+
+ internal val context: Context
+ private val appState: AppState
+ internal val handler: Handler
+ internal val pref: SharedPreferences
+ private val connectivityManager: ConnectivityManager
+ internal val notification_manager: NotificationManager
+ internal val scheduler: JobScheduler
+ private val power_manager: PowerManager?
+ internal val power_lock: PowerManager.WakeLock
+ private val wifi_manager: WifiManager?
+ internal val wifi_lock: WifiManager.WifiLock
+
+ private var worker: Worker
+
+ internal val job_list = LinkedList()
+
+ internal class Data(val access_info: SavedAccount, val notification: TootNotification)
+
+ internal class InjectData {
+
+ var account_db_id: Long = 0
+ val list = ArrayList()
+ }
+
+ init {
+ log.d("ctor")
+
+ val context = contextArg.applicationContext
+
+ this.context = context
+
+ // クラッシュレポートによると App1.onCreate より前にここを通る場合がある
+ // データベースへアクセスできるようにする
+ this.appState = App1.prepare(context, "PollingWorker.ctor()")
+ this.pref = App1.pref
+ this.handler = appState.handler
+
+ this.connectivityManager = systemService(context)
+ ?: error("missing ConnectivityManager system service")
+
+
+ this.notification_manager = systemService(context)
+ ?: error("missing NotificationManager system service")
+
+ this.scheduler = systemService(context)
+ ?: error("missing JobScheduler system service")
+
+ this.power_manager = systemService(context)
+ ?: error("missing PowerManager system service")
+
+ // WifiManagerの取得時はgetApplicationContext を使わないとlintに怒られる
+ this.wifi_manager = systemService(context.applicationContext)
+ ?: error("missing WifiManager system service")
+
+ power_lock = power_manager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
PollingWorker::class.java.name
)
- power_lock.setReferenceCounted(false)
-
- wifi_lock = if(Build.VERSION.SDK_INT >= 29) {
- wifi_manager.createWifiLock(
+ power_lock.setReferenceCounted(false)
+
+ wifi_lock = if (Build.VERSION.SDK_INT >= 29) {
+ wifi_manager.createWifiLock(
WifiManager.WIFI_MODE_FULL_HIGH_PERF,
PollingWorker::class.java.name
)
- } else {
- @Suppress("DEPRECATION")
- wifi_manager.createWifiLock(PollingWorker::class.java.name)
- }
-
- wifi_lock.setReferenceCounted(false)
-
- //
- worker = Worker()
- worker.start()
- }
-
- inner class Worker : WorkerBase() {
-
- val bThreadCancelled = AtomicBoolean(false)
-
- override fun cancel() {
- bThreadCancelled.set(true)
- notifyEx()
- }
-
- @SuppressLint("WakelockTimeout")
- private fun acquirePowerLock() {
- log.d("acquire power lock...")
- try {
- if(! power_lock.isHeld) {
- power_lock.acquire()
- }
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- try {
- if(! wifi_lock.isHeld) {
- wifi_lock.acquire()
- }
- } catch(ex : Throwable) {
- log.trace(ex)
- }
- }
-
- private fun releasePowerLock() {
- log.d("release power lock...")
- try {
- if(power_lock.isHeld) {
- power_lock.release()
- }
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- try {
- if(wifi_lock.isHeld) {
- wifi_lock.release()
- }
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- }
-
- override fun run() {
- log.d("worker thread start.")
- job_status.set("worker thread start.")
- while(! bThreadCancelled.get()) {
- try {
- val item : JobItem? = synchronized(job_list) {
- for(ji in job_list) {
- if(bThreadCancelled.get()) break
- if(ji.mJobCancelled_.get()) continue
- if(ji.mWorkerAttached.compareAndSet(false, true)) {
- return@synchronized ji
- }
- }
- null
- }
-
- if(item == null) {
- job_status.set("no job to run.")
- waitEx(86400000L)
- continue
- }
-
- job_status.set("start job " + item.jobId)
- acquirePowerLock()
- try {
- item.refWorker.set(this@Worker)
- item.run()
- } finally {
- job_status.set("end job " + item.jobId)
- item.refWorker.set(null)
- releasePowerLock()
- }
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- }
- job_status.set("worker thread end.")
- log.d("worker thread end.")
- }
- }
-
- //////////////////////////////////////////////////////////////////////
- // ジョブの管理
-
- // JobService#onDestroy から呼ばれる
- fun onJobServiceDestroy() {
- log.d("onJobServiceDestroy")
-
- synchronized(job_list) {
- val it = job_list.iterator()
- while(it.hasNext()) {
- val item = it.next()
- if(item.jobId != JOB_FCM) {
- it.remove()
- item.cancel(false)
- }
- }
- }
- }
-
- // JobService#onStartJob から呼ばれる
- fun onStartJob(jobService : JobService, params : JobParameters) : Boolean {
- val item = JobItem(jobService, params)
- addJob(item, true)
- return true
- // return True if your context needs to process the work (on a separate thread).
- // return False if there's no more work to be done for this job.
- }
-
- // FCMメッセージイベントから呼ばれる
- private fun hasJob(@Suppress("SameParameterValue") jobId : Int) : Boolean {
- synchronized(job_list) {
- for(item in job_list) {
- if(item.jobId == jobId) return true
- }
- }
- return false
- }
-
- // FCMメッセージイベントから呼ばれる
- private fun addJobFCM() {
- addJob(JobItem(JOB_FCM), false)
- }
-
- private fun addJob(item : JobItem, bRemoveOld : Boolean) {
- val jobId = item.jobId
-
- // 同じジョブ番号がジョブリストにあるか?
- synchronized(job_list) {
- if(bRemoveOld) {
- val it = job_list.iterator()
- while(it.hasNext()) {
- val itemOld = it.next()
- if(itemOld.jobId == jobId) {
- log.w("addJob: jobId=$jobId, old job cancelled.")
- // 同じジョブをすぐに始めるのだからrescheduleはfalse
- itemOld.cancel(false)
- it.remove()
- }
- }
- }
- log.d("addJob: jobId=$jobId, add to list.")
- job_list.add(item)
- }
-
- worker.notifyEx()
- }
-
- // JobService#onStopJob から呼ばれる
- fun onStopJob(params : JobParameters) : Boolean {
- val jobId = params.jobId
-
- // 同じジョブ番号がジョブリストにあるか?
- synchronized(job_list) {
- val it = job_list.iterator()
- while(it.hasNext()) {
- val item = it.next()
- if(item.jobId == jobId) {
- log.w("onStopJob: jobId=${jobId}, set cancel flag.")
- // リソースがなくてStopされるのだからrescheduleはtrue
- item.cancel(true)
- it.remove()
- return item.mReschedule.get()
- }
- }
- }
-
- // 該当するジョブを依頼されていない
- log.w("onStopJob: jobId=${jobId}, not started..")
- return false
- // return True to indicate to the JobManager whether you'd like to reschedule this job based on the retry criteria provided at job creation-time.
- // return False to drop the job. Regardless of the value returned, your job must stop executing.
- }
-
- internal class JobCancelledException : RuntimeException("job is cancelled.")
-
- inner class JobItem {
-
- val jobId : Int
- private val refJobService : WeakReference?
- private val jobParams : JobParameters?
- val mJobCancelled_ = AtomicBoolean()
- val mReschedule = AtomicBoolean()
- val mWorkerAttached = AtomicBoolean()
-
- val bPollingRequired = AtomicBoolean(false)
- lateinit var muted_app : HashSet
- lateinit var muted_word : WordTrieTree
- lateinit var favMuteSet : HashSet
- var bPollingComplete = false
- var install_id : String? = null
-
- var current_call : Call? = null
-
- val refWorker = AtomicReference(null)
-
- val isJobCancelled : Boolean
- get() {
- if(mJobCancelled_.get()) return true
- val worker = refWorker.get()
- return worker != null && worker.bThreadCancelled.get()
- }
-
- constructor(jobService : JobService, params : JobParameters) {
- this.jobParams = params
- this.jobId = params.jobId
- this.refJobService = WeakReference(jobService)
- }
-
- constructor(jobId : Int) {
- this.jobId = jobId
- this.jobParams = null
- this.refJobService = null
- }
-
- fun notifyWorkerThread() {
- val worker = refWorker.get()
- worker?.notifyEx()
- }
-
- fun waitWorkerThread(ms : Long) {
- val worker = refWorker.get()
- worker?.waitEx(ms)
- }
-
- fun cancel(bReschedule : Boolean) {
- mJobCancelled_.set(true)
- mReschedule.set(bReschedule)
- current_call?.cancel()
- notifyWorkerThread()
- }
-
- fun run() {
-
- job_status.set("job start.")
- try {
- log.d("(JobItem.run jobId=${jobId}")
- if(isJobCancelled) throw JobCancelledException()
-
- job_status.set("check network status..")
-
- val net_wait_start = SystemClock.elapsedRealtime()
- while(true) {
- val connectionState = App1.getAppState(context, "PollingWorker.JobItem.run()")
- .networkTracker.connectionState
- ?: break
- if(isJobCancelled) throw JobCancelledException()
- val now = SystemClock.elapsedRealtime()
- val delta = now - net_wait_start
- if(delta >= 10000L) {
- log.d("network state timeout. $connectionState")
- break
- }
- waitWorkerThread(333L)
- }
-
- muted_app = MutedApp.nameSet
- muted_word = MutedWord.nameSet
- favMuteSet = FavMute.acctSet
-
- // タスクがあれば処理する
- while(true) {
- if(isJobCancelled) throw JobCancelledException()
- val data = task_list.next(context) ?: break
- val task_id = data.optInt(EXTRA_TASK_ID, 0)
- TaskRunner().runTask(this@JobItem, task_id, data)
- }
-
- if(! isJobCancelled && ! bPollingComplete && jobId == JOB_POLLING) {
- // タスクがなかった場合でも定期実行ジョブからの実行ならポーリングを行う
- TaskRunner().runTask(this@JobItem, TASK_POLLING, JsonObject())
- }
- job_status.set("make next schedule.")
-
- log.d("pollingComplete=${bPollingComplete},isJobCancelled=${isJobCancelled},bPollingRequired=${bPollingRequired.get()}")
-
- if(! isJobCancelled && bPollingComplete) {
- // ポーリングが完了した
- if(! bPollingRequired.get()) {
- // Pull通知を必要とするアカウントが存在しないなら、スケジュール登録を解除する
- log.d("polling job is no longer required.")
- try {
- scheduler.cancel(JOB_POLLING)
- } catch(ex : Throwable) {
- log.trace(ex)
- }
- } else if(! scheduler.allPendingJobs.any { it.id == JOB_POLLING }) {
- // まだスケジュールされてないなら登録する
- log.d("registering polling job…")
- scheduleJob(context, JOB_POLLING)
- }
- }
- } catch(ex : JobCancelledException) {
- log.e("job execution cancelled.")
- } catch(ex : Throwable) {
- log.trace(ex)
- log.e(ex, "job execution failed.")
- } finally {
- job_status.set("job finished.")
- }
- // ジョブ終了報告
- if(! isJobCancelled) {
- handler.post(Runnable {
- if(isJobCancelled) return@Runnable
-
+ } else {
+ @Suppress("DEPRECATION")
+ wifi_manager.createWifiLock(PollingWorker::class.java.name)
+ }
+
+ wifi_lock.setReferenceCounted(false)
+
+ //
+ worker = Worker()
+ worker.start()
+ }
+
+ inner class Worker : WorkerBase() {
+
+ val bThreadCancelled = AtomicBoolean(false)
+
+ override fun cancel() {
+ bThreadCancelled.set(true)
+ notifyEx()
+ }
+
+ @SuppressLint("WakelockTimeout")
+ private fun acquirePowerLock() {
+ log.d("acquire power lock...")
+ try {
+ if (!power_lock.isHeld) {
+ power_lock.acquire()
+ }
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ try {
+ if (!wifi_lock.isHeld) {
+ wifi_lock.acquire()
+ }
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+ }
+
+ private fun releasePowerLock() {
+ log.d("release power lock...")
+ try {
+ if (power_lock.isHeld) {
+ power_lock.release()
+ }
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ try {
+ if (wifi_lock.isHeld) {
+ wifi_lock.release()
+ }
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ }
+
+ override fun run() {
+ log.d("worker thread start.")
+ job_status.set("worker thread start.")
+ while (!bThreadCancelled.get()) {
+ try {
+ val item: JobItem? = synchronized(job_list) {
+ for (ji in job_list) {
+ if (bThreadCancelled.get()) break
+ if (ji.mJobCancelled_.get()) continue
+ if (ji.mWorkerAttached.compareAndSet(false, true)) {
+ return@synchronized ji
+ }
+ }
+ null
+ }
+
+ if (item == null) {
+ job_status.set("no job to run.")
+ waitEx(86400000L)
+ continue
+ }
+
+ job_status.set("start job " + item.jobId)
+ acquirePowerLock()
+ try {
+ item.refWorker.set(this@Worker)
+ item.run()
+ } finally {
+ job_status.set("end job " + item.jobId)
+ item.refWorker.set(null)
+ releasePowerLock()
+ }
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ }
+ job_status.set("worker thread end.")
+ log.d("worker thread end.")
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////
+ // ジョブの管理
+
+ // JobService#onDestroy から呼ばれる
+ fun onJobServiceDestroy() {
+ log.d("onJobServiceDestroy")
+
+ synchronized(job_list) {
+ val it = job_list.iterator()
+ while (it.hasNext()) {
+ val item = it.next()
+ if (item.jobId != JOB_FCM) {
+ it.remove()
+ item.cancel(false)
+ }
+ }
+ }
+ }
+
+ // JobService#onStartJob から呼ばれる
+ fun onStartJob(jobService: JobService, params: JobParameters): Boolean {
+ val item = JobItem(jobService, params)
+ addJob(item, true)
+ return true
+ // return True if your context needs to process the work (on a separate thread).
+ // return False if there's no more work to be done for this job.
+ }
+
+ // FCMメッセージイベントから呼ばれる
+ private fun hasJob(@Suppress("SameParameterValue") jobId: Int): Boolean {
+ synchronized(job_list) {
+ for (item in job_list) {
+ if (item.jobId == jobId) return true
+ }
+ }
+ return false
+ }
+
+ // FCMメッセージイベントから呼ばれる
+ private fun addJobFCM() {
+ addJob(JobItem(JOB_FCM), false)
+ }
+
+ private fun addJob(item: JobItem, bRemoveOld: Boolean) {
+ val jobId = item.jobId
+
+ // 同じジョブ番号がジョブリストにあるか?
+ synchronized(job_list) {
+ if (bRemoveOld) {
+ val it = job_list.iterator()
+ while (it.hasNext()) {
+ val itemOld = it.next()
+ if (itemOld.jobId == jobId) {
+ log.w("addJob: jobId=$jobId, old job cancelled.")
+ // 同じジョブをすぐに始めるのだからrescheduleはfalse
+ itemOld.cancel(false)
+ it.remove()
+ }
+ }
+ }
+ log.d("addJob: jobId=$jobId, add to list.")
+ job_list.add(item)
+ }
+
+ worker.notifyEx()
+ }
+
+ // JobService#onStopJob から呼ばれる
+ fun onStopJob(params: JobParameters): Boolean {
+ val jobId = params.jobId
+
+ // 同じジョブ番号がジョブリストにあるか?
+ synchronized(job_list) {
+ val it = job_list.iterator()
+ while (it.hasNext()) {
+ val item = it.next()
+ if (item.jobId == jobId) {
+ log.w("onStopJob: jobId=${jobId}, set cancel flag.")
+ // リソースがなくてStopされるのだからrescheduleはtrue
+ item.cancel(true)
+ it.remove()
+ return item.mReschedule.get()
+ }
+ }
+ }
+
+ // 該当するジョブを依頼されていない
+ log.w("onStopJob: jobId=${jobId}, not started..")
+ return false
+ // return True to indicate to the JobManager whether you'd like to reschedule this job based on the retry criteria provided at job creation-time.
+ // return False to drop the job. Regardless of the value returned, your job must stop executing.
+ }
+
+ internal class JobCancelledException : RuntimeException("job is cancelled.")
+
+ inner class JobItem {
+
+ val jobId: Int
+ private val refJobService: WeakReference?
+ private val jobParams: JobParameters?
+ val mJobCancelled_ = AtomicBoolean()
+ val mReschedule = AtomicBoolean()
+ val mWorkerAttached = AtomicBoolean()
+
+ val bPollingRequired = AtomicBoolean(false)
+ lateinit var muted_app: HashSet
+ lateinit var muted_word: WordTrieTree
+ lateinit var favMuteSet: HashSet
+ var bPollingComplete = false
+ var install_id: String? = null
+
+ var current_call: Call? = null
+
+ val refWorker = AtomicReference(null)
+
+ val isJobCancelled: Boolean
+ get() {
+ if (mJobCancelled_.get()) return true
+ val worker = refWorker.get()
+ return worker != null && worker.bThreadCancelled.get()
+ }
+
+ constructor(jobService: JobService, params: JobParameters) {
+ this.jobParams = params
+ this.jobId = params.jobId
+ this.refJobService = WeakReference(jobService)
+ }
+
+ constructor(jobId: Int) {
+ this.jobId = jobId
+ this.jobParams = null
+ this.refJobService = null
+ }
+
+ fun notifyWorkerThread() {
+ val worker = refWorker.get()
+ worker?.notifyEx()
+ }
+
+ fun waitWorkerThread(ms: Long) {
+ val worker = refWorker.get()
+ worker?.waitEx(ms)
+ }
+
+ fun cancel(bReschedule: Boolean) {
+ mJobCancelled_.set(true)
+ mReschedule.set(bReschedule)
+ current_call?.cancel()
+ notifyWorkerThread()
+ }
+
+ fun run() {
+
+ job_status.set("job start.")
+ try {
+ log.d("(JobItem.run jobId=${jobId}")
+ if (isJobCancelled) throw JobCancelledException()
+
+ job_status.set("check network status..")
+
+ val net_wait_start = SystemClock.elapsedRealtime()
+ while (true) {
+ val connectionState = App1.getAppState(context, "PollingWorker.JobItem.run()")
+ .networkTracker.connectionState
+ ?: break
+ if (isJobCancelled) throw JobCancelledException()
+ val now = SystemClock.elapsedRealtime()
+ val delta = now - net_wait_start
+ if (delta >= 10000L) {
+ log.d("network state timeout. $connectionState")
+ break
+ }
+ waitWorkerThread(333L)
+ }
+
+ muted_app = MutedApp.nameSet
+ muted_word = MutedWord.nameSet
+ favMuteSet = FavMute.acctSet
+
+ // タスクがあれば処理する
+ while (true) {
+ if (isJobCancelled) throw JobCancelledException()
+ val data = task_list.next(context) ?: break
+ val task_id = data.optInt(EXTRA_TASK_ID, 0)
+ TaskRunner().runTask(this@JobItem, task_id, data)
+ }
+
+ if (!isJobCancelled && !bPollingComplete && jobId == JOB_POLLING) {
+ // タスクがなかった場合でも定期実行ジョブからの実行ならポーリングを行う
+ TaskRunner().runTask(this@JobItem, TASK_POLLING, JsonObject())
+ }
+ job_status.set("make next schedule.")
+
+ log.d("pollingComplete=${bPollingComplete},isJobCancelled=${isJobCancelled},bPollingRequired=${bPollingRequired.get()}")
+
+ if (!isJobCancelled && bPollingComplete) {
+ // ポーリングが完了した
+ if (!bPollingRequired.get()) {
+ // Pull通知を必要とするアカウントが存在しないなら、スケジュール登録を解除する
+ log.d("polling job is no longer required.")
+ try {
+ scheduler.cancel(JOB_POLLING)
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+ } else if (!scheduler.allPendingJobs.any { it.id == JOB_POLLING }) {
+ // まだスケジュールされてないなら登録する
+ log.d("registering polling job…")
+ scheduleJob(context, JOB_POLLING)
+ }
+ }
+ } catch (ex: JobCancelledException) {
+ log.e("job execution cancelled.")
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ log.e(ex, "job execution failed.")
+ } finally {
+ job_status.set("job finished.")
+ }
+ // ジョブ終了報告
+ if (!isJobCancelled) {
+ handler.post(Runnable {
+ if (isJobCancelled) return@Runnable
+
synchronized(job_list) {
job_list.remove(this@JobItem)
}
-
+
try {
val jobService = refJobService?.get()
- if(jobService != null) {
+ if (jobService != null) {
val willReschedule = mReschedule.get()
log.d("sending jobFinished. willReschedule=$willReschedule")
jobService.jobFinished(jobParams, willReschedule)
}
- } catch(ex : Throwable) {
+ } catch (ex: Throwable) {
log.trace(ex, "jobFinished failed(1).")
}
})
- }
- log.d(")JobItem.run jobId=${jobId}, cancel=${isJobCancelled}")
- }
-
- }
-
- private fun TrackingType.trackingTypeName() = when(this) {
+ }
+ log.d(")JobItem.run jobId=${jobId}, cancel=${isJobCancelled}")
+ }
+
+ }
+
+ private fun TrackingType.trackingTypeName() = when (this) {
TrackingType.NotReply -> NotificationHelper.TRACKING_NAME_DEFAULT
TrackingType.Reply -> NotificationHelper.TRACKING_NAME_REPLY
TrackingType.All -> NotificationHelper.TRACKING_NAME_DEFAULT
- }
-
- internal inner class TaskRunner {
-
- lateinit var job : JobItem
- private var taskId : Int = 0
-
- val error_instance = ArrayList()
-
- fun runTask(job : JobItem, taskId : Int, taskData : JsonObject) {
- try {
- log.d("(runTask: taskId=${taskId}")
- job_status.set("start task $taskId")
-
- this.job = job
- this.taskId = taskId
-
- var process_db_id = - 1L //
-
- when(taskId) {
+ }
+
+ internal inner class TaskRunner {
+
+ lateinit var job: JobItem
+ private var taskId: Int = 0
+
+ val error_instance = ArrayList()
+
+ fun runTask(job: JobItem, taskId: Int, taskData: JsonObject) {
+ try {
+ log.d("(runTask: taskId=${taskId}")
+ job_status.set("start task $taskId")
+
+ this.job = job
+ this.taskId = taskId
+
+ var process_db_id = -1L //
+
+ when (taskId) {
TASK_APP_DATA_IMPORT_BEFORE -> {
scheduler.cancelAll()
- for(a in SavedAccount.loadAccountList(context)) {
+ for (a in SavedAccount.loadAccountList(context)) {
try {
val notification_tag = a.db_id.toString()
notification_manager.cancel(notification_tag, NOTIFICATION_ID)
- } catch(ex : Throwable) {
+ } catch (ex: Throwable) {
log.trace(ex)
}
-
+
}
mBusyAppDataImportBefore.set(false)
return
}
-
+
TASK_APP_DATA_IMPORT_AFTER -> {
mBusyAppDataImportAfter.set(false)
mBusyAppDataImportBefore.set(false)
NotificationTracking.resetPostAll()
// fall
}
-
- }
-
- // アプリデータのインポート処理がビジーな間、他のジョブは実行されない
- if(mBusyAppDataImportBefore.get() || mBusyAppDataImportAfter.get()) return
-
- // タスクによってはポーリング前にすることがある
- when(taskId) {
+
+ }
+
+ // アプリデータのインポート処理がビジーな間、他のジョブは実行されない
+ if (mBusyAppDataImportBefore.get() || mBusyAppDataImportAfter.get()) return
+
+ // タスクによってはポーリング前にすることがある
+ when (taskId) {
TASK_DATA_INJECTED -> processInjectedData()
-
+
TASK_BOOT_COMPLETED -> NotificationTracking.resetPostAll()
-
+
TASK_PACKAGE_REPLACED -> NotificationTracking.resetPostAll()
-
- // デバイストークンが更新された
+
+ // デバイストークンが更新された
TASK_FCM_DEVICE_TOKEN -> {
}
-
- // プッシュ通知が届いた
+
+ // プッシュ通知が届いた
TASK_FCM_MESSAGE -> {
var bDone = false
val tag = taskData.string(EXTRA_TAG)
- if(tag != null) {
- if(tag.startsWith("acct<>")) {
+ if (tag != null) {
+ if (tag.startsWith("acct<>")) {
val acct = tag.substring(6)
val sa = SavedAccount.loadAccountByAcct(context, acct)
- if(sa != null) {
+ if (sa != null) {
NotificationCache.resetLastLoad(sa.db_id)
process_db_id = sa.db_id
bDone = true
}
}
- if(! bDone) {
- for(sa in SavedAccount.loadByTag(context, tag)) {
+ if (!bDone) {
+ for (sa in SavedAccount.loadByTag(context, tag)) {
NotificationCache.resetLastLoad(sa.db_id)
process_db_id = sa.db_id
bDone = true
}
}
}
- if(! bDone) {
+ if (!bDone) {
// タグにマッチする情報がなかった場合、全部読み直す
NotificationCache.resetLastLoad()
}
}
-
+
TASK_NOTIFICATION_CLEAR -> {
val db_id = taskData.long(EXTRA_DB_ID)
log.d("Notification clear! db_id=$db_id")
- if(db_id != null) {
+ if (db_id != null) {
deleteCacheData(db_id)
}
}
-
+
TASK_NOTIFICATION_DELETE -> {
val db_id = taskData.long(EXTRA_DB_ID)
val type = TrackingType.parseStr(taskData.string(EXTRA_NOTIFICATION_TYPE))
val typeName = type.trackingTypeName()
val id = taskData.string(EXTRA_NOTIFICATION_ID)
log.d("Notification deleted! db_id=$db_id,type=$type,id=$id")
- if(db_id != null) {
+ if (db_id != null) {
NotificationTracking.updateRead(db_id, typeName)
}
return
}
-
+
TASK_NOTIFICATION_CLICK -> {
val db_id = taskData.long(EXTRA_DB_ID)
val type = TrackingType.parseStr(taskData.string(EXTRA_NOTIFICATION_TYPE))
val typeName = type.trackingTypeName()
val id = taskData.string(EXTRA_NOTIFICATION_ID).notEmpty()
log.d("Notification clicked! db_id=$db_id,type=$type,id=$id")
- if(db_id != null) {
+ if (db_id != null) {
// 通知をキャンセル
- val notification_tag = when(typeName) {
+ val notification_tag = when (typeName) {
"" -> "${db_id}/_"
else -> "${db_id}/$typeName"
}
- if(id != null) {
+ if (id != null) {
val itemTag = "$notification_tag/$id"
notification_manager.cancel(itemTag, NOTIFICATION_ID)
} else {
@@ -976,130 +971,132 @@ class PollingWorker private constructor(contextArg : Context) {
}
return
}
-
- }
-
- job_status.set("make install id")
-
- // インストールIDを生成する
- // インストールID生成時にSavedAccountテーブルを操作することがあるので
- // アカウントリストの取得より先に行う
- if(job.install_id == null) {
- job.install_id = prepareInstallId(context, job)
- }
-
- // アカウント別に処理スレッドを作る
- job_status.set("create account thread")
- val thread_list = LinkedList()
- for(_a in SavedAccount.loadAccountList(context)) {
- if(_a.isPseudo) continue
- if(process_db_id != - 1L && _a.db_id != process_db_id) continue
- val t = AccountThread(_a)
- thread_list.add(t)
- t.start()
- }
- while(true) {
- // 同じホスト名が重複しないようにSetに集める
- val liveSet = TreeSet()
- for(t in thread_list) {
- if(! t.isAlive) continue
- if(job.isJobCancelled) t.cancel()
- liveSet.add(t.account.apiHost)
- }
- if(liveSet.isEmpty()) break
-
- job_status.set("waiting " + liveSet.joinToString(", ") { it.pretty })
- job.waitWorkerThread(if(job.isJobCancelled) 100L else 1000L)
- }
-
- synchronized(error_instance) {
- createErrorNotification(error_instance)
- }
-
- if(! job.isJobCancelled) job.bPollingComplete = true
-
- } catch(ex : Throwable) {
- log.trace(ex, "task execution failed.")
- } finally {
- log.d(")runTask: taskId=$taskId")
- job_status.set("end task $taskId")
- }
- }
-
- internal inner class AccountThread(
- val account : SavedAccount
+
+ }
+
+ job_status.set("make install id")
+
+ // インストールIDを生成する
+ // インストールID生成時にSavedAccountテーブルを操作することがあるので
+ // アカウントリストの取得より先に行う
+ if (job.install_id == null) {
+ job.install_id = runBlocking { prepareInstallId(context, job) }
+ }
+
+ // アカウント別に処理スレッドを作る
+ job_status.set("create account thread")
+ val thread_list = LinkedList()
+ for (_a in SavedAccount.loadAccountList(context)) {
+ if (_a.isPseudo) continue
+ if (process_db_id != -1L && _a.db_id != process_db_id) continue
+ val t = AccountThread(_a)
+ thread_list.add(t)
+ t.start()
+ }
+ while (true) {
+ // 同じホスト名が重複しないようにSetに集める
+ val liveSet = TreeSet()
+ for (t in thread_list) {
+ if (!t.isAlive) continue
+ if (job.isJobCancelled) t.cancel()
+ liveSet.add(t.account.apiHost)
+ }
+ if (liveSet.isEmpty()) break
+
+ job_status.set("waiting " + liveSet.joinToString(", ") { it.pretty })
+ job.waitWorkerThread(if (job.isJobCancelled) 100L else 1000L)
+ }
+
+ synchronized(error_instance) {
+ createErrorNotification(error_instance)
+ }
+
+ if (!job.isJobCancelled) job.bPollingComplete = true
+
+ } catch (ex: Throwable) {
+ log.trace(ex, "task execution failed.")
+ } finally {
+ log.d(")runTask: taskId=$taskId")
+ job_status.set("end task $taskId")
+ }
+ }
+
+ internal inner class AccountThread(
+ val account: SavedAccount
) : Thread(), CurrentCallCallback {
-
- private var current_call : Call? = null
-
- private val client = TootApiClient(context, callback = object : TootApiCallback {
- override val isApiCancelled : Boolean
+
+ private var current_call: Call? = null
+
+ private val client = TootApiClient(context, callback = object : TootApiCallback {
+ override val isApiCancelled: Boolean
get() = job.isJobCancelled
})
-
- private val favMuteSet : HashSet get() = job.favMuteSet
- private lateinit var parser : TootParser
- private lateinit var cache : NotificationCache
-
- init {
- client.currentCallCallback = this
- }
-
- override fun onCallCreated(call : Call) {
- current_call = call
- }
-
- fun cancel() {
- try {
- current_call?.cancel()
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- }
-
- override fun run() {
+
+ private val favMuteSet: HashSet get() = job.favMuteSet
+ private lateinit var parser: TootParser
+ private lateinit var cache: NotificationCache
+
+ init {
+ client.currentCallCallback = this
+ }
+
+ override fun onCallCreated(call: Call) {
+ current_call = call
+ }
+
+ fun cancel() {
+ try {
+ current_call?.cancel()
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ }
+
+ override fun run() = runBlocking { runSuspend() }
+
+ private suspend fun runSuspend(){
try {
// 疑似アカウントはチェック対象外
- if(account.isPseudo) return
-
+ if (account.isPseudo) return
+
// 未確認アカウントはチェック対象外
- if(! account.isConfirmed) return
-
+ if (!account.isConfirmed) return
+
client.account = account
-
+
val wps = PushSubscriptionHelper(context, account)
-
- if(wps.flags != 0) {
+
+ if (wps.flags != 0) {
job.bPollingRequired.set(true)
-
+
val (instance, instanceResult) = TootInstance.get(client)
- if(instance == null) {
- if(instanceResult != null) {
+ if (instance == null) {
+ if (instanceResult != null) {
log.e("${instanceResult.error} ${instanceResult.requestInfo}".trim())
account.updateNotificationError("${instanceResult.error} ${instanceResult.requestInfo}".trim())
}
return
}
-
- if(job.isJobCancelled) return
+
+ if (job.isJobCancelled) return
}
-
+
wps.updateSubscription(client) ?: return // cancelled.
-
+
val wps_log = wps.log
- if(wps_log.isNotEmpty())
+ if (wps_log.isNotEmpty())
log.d("PushSubscriptionHelper: ${account.acct.pretty} $wps_log")
-
- if(job.isJobCancelled) return
-
- if(wps.flags == 0) {
- if(account.last_notification_error != null) {
+
+ if (job.isJobCancelled) return
+
+ if (wps.flags == 0) {
+ if (account.last_notification_error != null) {
account.updateNotificationError(null)
}
return
}
-
+
this.cache = NotificationCache(account.db_id).apply {
load()
request(
@@ -1108,16 +1105,16 @@ class PollingWorker private constructor(contextArg : Context) {
wps.flags,
onError = { result ->
val sv = result.error
- if(sv?.contains("Timeout") == true && ! account.dont_show_timeout) {
+ if (sv?.contains("Timeout") == true && !account.dont_show_timeout) {
synchronized(error_instance) {
var bFound = false
- for(x in error_instance) {
- if(x == sv) {
+ for (x in error_instance) {
+ if (x == sv) {
bFound = true
break
}
}
- if(! bFound) {
+ if (!bFound) {
error_instance.add(sv)
}
}
@@ -1128,17 +1125,17 @@ class PollingWorker private constructor(contextArg : Context) {
}
)
}
- if(job.isJobCancelled) return
-
+ if (job.isJobCancelled) return
+
this.parser = TootParser(context, account)
-
- if(Pref.bpSeparateReplyNotificationGroup(pref)) {
+
+ if (Pref.bpSeparateReplyNotificationGroup(pref)) {
var tr = TrackingRunner(
trackingType = TrackingType.NotReply,
trackingName = NotificationHelper.TRACKING_NAME_DEFAULT
)
tr.checkAccount()
- if(job.isJobCancelled) return
+ if (job.isJobCancelled) return
tr.updateNotification()
//
tr = TrackingRunner(
@@ -1146,372 +1143,372 @@ class PollingWorker private constructor(contextArg : Context) {
trackingName = NotificationHelper.TRACKING_NAME_REPLY
)
tr.checkAccount()
- if(job.isJobCancelled) return
+ if (job.isJobCancelled) return
tr.updateNotification()
-
+
} else {
val tr = TrackingRunner(
trackingType = TrackingType.All,
trackingName = NotificationHelper.TRACKING_NAME_DEFAULT
)
tr.checkAccount()
- if(job.isJobCancelled) return
+ if (job.isJobCancelled) return
tr.updateNotification()
}
-
- } catch(ex : Throwable) {
+
+ } catch (ex: Throwable) {
log.trace(ex)
} finally {
job.notifyWorkerThread()
}
}
-
- inner class TrackingRunner(
- var trackingType : TrackingType = TrackingType.All,
- var trackingName : String = ""
+
+ inner class TrackingRunner(
+ var trackingType: TrackingType = TrackingType.All,
+ var trackingName: String = ""
) {
-
- private lateinit var nr : NotificationTracking
- private val duplicate_check = HashSet()
- private val dstListData = LinkedList()
-
- internal fun checkAccount() {
-
- this.nr = NotificationTracking.load(account.db_id, trackingName)
-
- val jsonList = when(trackingType) {
+
+ private lateinit var nr: NotificationTracking
+ private val duplicate_check = HashSet()
+ private val dstListData = LinkedList()
+
+ internal fun checkAccount() {
+
+ this.nr = NotificationTracking.load(account.db_id, trackingName)
+
+ val jsonList = when (trackingType) {
TrackingType.All -> cache.data
TrackingType.Reply -> cache.data.filter {
- when(parseNotificationType(account, it)) {
+ when (parseNotificationType(account, it)) {
TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true
else -> false
}
}
TrackingType.NotReply -> cache.data.filter {
- ! when(parseNotificationType(account, it)) {
+ !when (parseNotificationType(account, it)) {
TootNotification.TYPE_REPLY, TootNotification.TYPE_MENTION -> true
else -> false
}
}
- }
-
- // 新しい順に並んでいる。先頭から10件までを処理する。ただし処理順序は古い方から
- val size = min(10, jsonList.size)
- for(i in (0 until size).reversed()) {
- if(job.isJobCancelled) return
- update_sub(jsonList[i])
- }
- if(job.isJobCancelled) return
-
- // 種別チェックより先に、cache中の最新のIDを「最後に表示した通知」に指定する
- // nid_show は通知タップ時に参照されるので、通知を表示する際は必ず更新・保存する必要がある
- // 種別チェックより優先する
- if(cache.sinceId != null) nr.nid_show = cache.sinceId
- nr.save()
- }
-
- private fun update_sub(src : JsonObject) {
-
- val id = getEntityOrderId(account, src)
- if(id.isDefault || duplicate_check.contains(id)) return
- duplicate_check.add(id)
-
- // タップ・削除した通知のIDと同じか古いなら対象外
- if(! id.isNewerThan(nr.nid_read)) return
-
- log.d("update_sub: found data that id=${id}, > read id ${nr.nid_read}")
-
- val notification = parser.notification(src) ?: return
-
- // アプリミュートと単語ミュート
- if(notification.status?.checkMuted() == true) return
-
- // ふぁぼ魔ミュート
- when(notification.type) {
+ }
+
+ // 新しい順に並んでいる。先頭から10件までを処理する。ただし処理順序は古い方から
+ val size = min(10, jsonList.size)
+ for (i in (0 until size).reversed()) {
+ if (job.isJobCancelled) return
+ update_sub(jsonList[i])
+ }
+ if (job.isJobCancelled) return
+
+ // 種別チェックより先に、cache中の最新のIDを「最後に表示した通知」に指定する
+ // nid_show は通知タップ時に参照されるので、通知を表示する際は必ず更新・保存する必要がある
+ // 種別チェックより優先する
+ if (cache.sinceId != null) nr.nid_show = cache.sinceId
+ nr.save()
+ }
+
+ private fun update_sub(src: JsonObject) {
+
+ val id = getEntityOrderId(account, src)
+ if (id.isDefault || duplicate_check.contains(id)) return
+ duplicate_check.add(id)
+
+ // タップ・削除した通知のIDと同じか古いなら対象外
+ if (!id.isNewerThan(nr.nid_read)) return
+
+ log.d("update_sub: found data that id=${id}, > read id ${nr.nid_read}")
+
+ val notification = parser.notification(src) ?: return
+
+ // アプリミュートと単語ミュート
+ if (notification.status?.checkMuted() == true) return
+
+ // ふぁぼ魔ミュート
+ when (notification.type) {
TootNotification.TYPE_REBLOG,
TootNotification.TYPE_FAVOURITE,
TootNotification.TYPE_FOLLOW,
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY -> {
val who = notification.account
- if(who != null && favMuteSet.contains(account.getFullAcct(who))) {
+ if (who != null && favMuteSet.contains(account.getFullAcct(who))) {
log.d("${account.getFullAcct(who)} is in favMuteSet.")
return
}
}
- }
-
- // 後から処理したものが先頭に来る
- dstListData.add(0, Data(account, notification))
- }
-
- internal fun updateNotification() {
-
- val notification_tag = when(trackingName) {
+ }
+
+ // 後から処理したものが先頭に来る
+ dstListData.add(0, Data(account, notification))
+ }
+
+ internal fun updateNotification() {
+
+ val notification_tag = when (trackingName) {
"" -> "${account.db_id}/_"
- else -> "${account.db_id}/$trackingName"
- }
-
- val nt = NotificationTracking.load(account.db_id, trackingName)
- val dataList = dstListData
- val first = dataList.firstOrNull()
- if(first == null) {
- log.d("showNotification[${account.acct.pretty}/$notification_tag] cancel notification.")
- if(Build.VERSION.SDK_INT >= 23 && Pref.bpDivideNotification(pref)) {
- notification_manager.activeNotifications?.forEach {
- if(it != null &&
- it.id == NOTIFICATION_ID &&
- it.tag.startsWith("$notification_tag/")
- ) {
- log.d("cancel: ${it.tag} context=${account.acct.pretty} $notification_tag")
- notification_manager.cancel(it.tag, NOTIFICATION_ID)
- }
- }
- } else {
- notification_manager.cancel(notification_tag, NOTIFICATION_ID)
- }
- return
- }
-
- val lastPostTime = nt.post_time
- val lastPostId = nt.post_id
- if(first.notification.time_created_at == lastPostTime
- && first.notification.id == lastPostId
- ) {
- // 先頭にあるデータが同じなら、通知を更新しない
- // このマーカーは端末再起動時にリセットされるので、再起動後は通知が出るはず
- log.d("showNotification[${account.acct.pretty}] id=${first.notification.id} is already shown.")
- return
- }
-
- if(Build.VERSION.SDK_INT >= 23 && Pref.bpDivideNotification(pref)) {
- val activeNotificationMap = HashMap().apply {
- notification_manager.activeNotifications?.forEach {
- if(it != null &&
- it.id == NOTIFICATION_ID &&
- it.tag.startsWith("$notification_tag/")
- ) {
- put(it.tag, it)
- }
- }
- }
- for(item in dstListData.reversed()) {
- val itemTag = "$notification_tag/${item.notification.id}"
-
- if(lastPostId != null &&
- item.notification.time_created_at <= lastPostTime &&
- item.notification.id <= lastPostId
- ) {
- // 掲載済みデータより古い通知は再表示しない
- log.d("ignore $itemTag ${item.notification.time_created_at} <= $lastPostTime && ${item.notification.id} <= $lastPostId")
- continue
- }
-
- // ignore if already showing
- if(activeNotificationMap.remove(itemTag) != null) {
- log.d("ignore $itemTag is in activeNotificationMap")
- continue
- }
-
- createNotification(
+ else -> "${account.db_id}/$trackingName"
+ }
+
+ val nt = NotificationTracking.load(account.db_id, trackingName)
+ val dataList = dstListData
+ val first = dataList.firstOrNull()
+ if (first == null) {
+ log.d("showNotification[${account.acct.pretty}/$notification_tag] cancel notification.")
+ if (Build.VERSION.SDK_INT >= 23 && Pref.bpDivideNotification(pref)) {
+ notification_manager.activeNotifications?.forEach {
+ if (it != null &&
+ it.id == NOTIFICATION_ID &&
+ it.tag.startsWith("$notification_tag/")
+ ) {
+ log.d("cancel: ${it.tag} context=${account.acct.pretty} $notification_tag")
+ notification_manager.cancel(it.tag, NOTIFICATION_ID)
+ }
+ }
+ } else {
+ notification_manager.cancel(notification_tag, NOTIFICATION_ID)
+ }
+ return
+ }
+
+ val lastPostTime = nt.post_time
+ val lastPostId = nt.post_id
+ if (first.notification.time_created_at == lastPostTime
+ && first.notification.id == lastPostId
+ ) {
+ // 先頭にあるデータが同じなら、通知を更新しない
+ // このマーカーは端末再起動時にリセットされるので、再起動後は通知が出るはず
+ log.d("showNotification[${account.acct.pretty}] id=${first.notification.id} is already shown.")
+ return
+ }
+
+ if (Build.VERSION.SDK_INT >= 23 && Pref.bpDivideNotification(pref)) {
+ val activeNotificationMap = HashMap().apply {
+ notification_manager.activeNotifications?.forEach {
+ if (it != null &&
+ it.id == NOTIFICATION_ID &&
+ it.tag.startsWith("$notification_tag/")
+ ) {
+ put(it.tag, it)
+ }
+ }
+ }
+ for (item in dstListData.reversed()) {
+ val itemTag = "$notification_tag/${item.notification.id}"
+
+ if (lastPostId != null &&
+ item.notification.time_created_at <= lastPostTime &&
+ item.notification.id <= lastPostId
+ ) {
+ // 掲載済みデータより古い通知は再表示しない
+ log.d("ignore $itemTag ${item.notification.time_created_at} <= $lastPostTime && ${item.notification.id} <= $lastPostId")
+ continue
+ }
+
+ // ignore if already showing
+ if (activeNotificationMap.remove(itemTag) != null) {
+ log.d("ignore $itemTag is in activeNotificationMap")
+ continue
+ }
+
+ createNotification(
itemTag,
notificationId = item.notification.id.toString()
) { builder ->
-
- builder.setWhen(item.notification.time_created_at)
-
- val summary = getNotificationLine(item)
- builder.setContentTitle(summary)
- val content = item.notification.status?.decoded_content?.notEmpty()
- if(content != null) {
- builder.setStyle(
+
+ builder.setWhen(item.notification.time_created_at)
+
+ val summary = getNotificationLine(item)
+ builder.setContentTitle(summary)
+ val content = item.notification.status?.decoded_content?.notEmpty()
+ if (content != null) {
+ builder.setStyle(
NotificationCompat.BigTextStyle()
.setBigContentTitle(summary)
.setSummaryText(item.access_info.acct.pretty)
.bigText(content)
)
- } else {
- builder.setContentText(item.access_info.acct.pretty)
- }
-
- if(Build.VERSION.SDK_INT < 26) {
- var iv = 0
-
- if(Pref.bpNotificationSound(pref)) {
-
- var sound_uri : Uri? = null
-
- try {
- val whoAcct =
- account.getFullAcct(item.notification.account)
- sound_uri =
- AcctColor.getNotificationSound(whoAcct).mayUri()
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- if(sound_uri == null) {
- sound_uri = account.sound_uri.mayUri()
- }
-
- var bSoundSet = false
- if(sound_uri != null) {
- try {
- builder.setSound(sound_uri)
- bSoundSet = true
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- }
- if(! bSoundSet) {
- iv = iv or NotificationCompat.DEFAULT_SOUND
- }
- }
-
- if(Pref.bpNotificationVibration(pref)) {
- iv = iv or NotificationCompat.DEFAULT_VIBRATE
- }
-
- if(Pref.bpNotificationLED(pref)) {
- iv = iv or NotificationCompat.DEFAULT_LIGHTS
- }
-
- builder.setDefaults(iv)
- }
- }
- }
- // リストにない通知は消さない。ある通知をユーザが指で削除した際に他の通知が残ってほしい場合がある
- } else {
- log.d("showNotification[${account.acct.pretty}] creating notification(1)")
- createNotification(notification_tag) { builder ->
-
- builder.setWhen(first.notification.time_created_at)
-
- var a = getNotificationLine(first)
-
- if(dataList.size == 1) {
- builder.setContentTitle(a)
- builder.setContentText(account.acct.pretty)
- } else {
- val header =
- context.getString(R.string.notification_count, dataList.size)
- builder.setContentTitle(header)
- .setContentText(a)
-
- val style = NotificationCompat.InboxStyle()
- .setBigContentTitle(header)
- .setSummaryText(account.acct.pretty)
- for(i in 0 .. 4) {
- if(i >= dataList.size) break
- val item = dataList[i]
- a = getNotificationLine(item)
- style.addLine(a)
- }
- builder.setStyle(style)
- }
-
- if(Build.VERSION.SDK_INT < 26) {
-
- var iv = 0
-
- if(Pref.bpNotificationSound(pref)) {
-
- var sound_uri : Uri? = null
-
- try {
- val whoAcct =
- account.getFullAcct(first.notification.account)
- sound_uri = AcctColor.getNotificationSound(whoAcct).mayUri()
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- if(sound_uri == null) {
- sound_uri = account.sound_uri.mayUri()
- }
-
- var bSoundSet = false
- if(sound_uri != null) {
- try {
- builder.setSound(sound_uri)
- bSoundSet = true
- } catch(ex : Throwable) {
- log.trace(ex)
- }
-
- }
- if(! bSoundSet) {
- iv = iv or NotificationCompat.DEFAULT_SOUND
- }
- }
-
- if(Pref.bpNotificationVibration(pref)) {
- iv = iv or NotificationCompat.DEFAULT_VIBRATE
- }
-
- if(Pref.bpNotificationLED(pref)) {
- iv = iv or NotificationCompat.DEFAULT_LIGHTS
- }
-
- builder.setDefaults(iv)
- }
- }
- }
- nt.updatePost(first.notification.id, first.notification.time_created_at)
- }
-
- private fun createNotification(
- notification_tag : String,
- notificationId : String? = null,
- setContent : (builder : NotificationCompat.Builder) -> Unit
+ } else {
+ builder.setContentText(item.access_info.acct.pretty)
+ }
+
+ if (Build.VERSION.SDK_INT < 26) {
+ var iv = 0
+
+ if (Pref.bpNotificationSound(pref)) {
+
+ var sound_uri: Uri? = null
+
+ try {
+ val whoAcct =
+ account.getFullAcct(item.notification.account)
+ sound_uri =
+ AcctColor.getNotificationSound(whoAcct).mayUri()
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ if (sound_uri == null) {
+ sound_uri = account.sound_uri.mayUri()
+ }
+
+ var bSoundSet = false
+ if (sound_uri != null) {
+ try {
+ builder.setSound(sound_uri)
+ bSoundSet = true
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ }
+ if (!bSoundSet) {
+ iv = iv or NotificationCompat.DEFAULT_SOUND
+ }
+ }
+
+ if (Pref.bpNotificationVibration(pref)) {
+ iv = iv or NotificationCompat.DEFAULT_VIBRATE
+ }
+
+ if (Pref.bpNotificationLED(pref)) {
+ iv = iv or NotificationCompat.DEFAULT_LIGHTS
+ }
+
+ builder.setDefaults(iv)
+ }
+ }
+ }
+ // リストにない通知は消さない。ある通知をユーザが指で削除した際に他の通知が残ってほしい場合がある
+ } else {
+ log.d("showNotification[${account.acct.pretty}] creating notification(1)")
+ createNotification(notification_tag) { builder ->
+
+ builder.setWhen(first.notification.time_created_at)
+
+ var a = getNotificationLine(first)
+
+ if (dataList.size == 1) {
+ builder.setContentTitle(a)
+ builder.setContentText(account.acct.pretty)
+ } else {
+ val header =
+ context.getString(R.string.notification_count, dataList.size)
+ builder.setContentTitle(header)
+ .setContentText(a)
+
+ val style = NotificationCompat.InboxStyle()
+ .setBigContentTitle(header)
+ .setSummaryText(account.acct.pretty)
+ for (i in 0..4) {
+ if (i >= dataList.size) break
+ val item = dataList[i]
+ a = getNotificationLine(item)
+ style.addLine(a)
+ }
+ builder.setStyle(style)
+ }
+
+ if (Build.VERSION.SDK_INT < 26) {
+
+ var iv = 0
+
+ if (Pref.bpNotificationSound(pref)) {
+
+ var sound_uri: Uri? = null
+
+ try {
+ val whoAcct =
+ account.getFullAcct(first.notification.account)
+ sound_uri = AcctColor.getNotificationSound(whoAcct).mayUri()
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ if (sound_uri == null) {
+ sound_uri = account.sound_uri.mayUri()
+ }
+
+ var bSoundSet = false
+ if (sound_uri != null) {
+ try {
+ builder.setSound(sound_uri)
+ bSoundSet = true
+ } catch (ex: Throwable) {
+ log.trace(ex)
+ }
+
+ }
+ if (!bSoundSet) {
+ iv = iv or NotificationCompat.DEFAULT_SOUND
+ }
+ }
+
+ if (Pref.bpNotificationVibration(pref)) {
+ iv = iv or NotificationCompat.DEFAULT_VIBRATE
+ }
+
+ if (Pref.bpNotificationLED(pref)) {
+ iv = iv or NotificationCompat.DEFAULT_LIGHTS
+ }
+
+ builder.setDefaults(iv)
+ }
+ }
+ }
+ nt.updatePost(first.notification.id, first.notification.time_created_at)
+ }
+
+ private fun createNotification(
+ notification_tag: String,
+ notificationId: String? = null,
+ setContent: (builder: NotificationCompat.Builder) -> Unit
) {
- log.d("showNotification[${account.acct.pretty}] creating notification(1)")
-
- val builder = if(Build.VERSION.SDK_INT >= 26) {
- // Android 8 から、通知のスタイルはユーザが管理することになった
- // NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる
- val channel = NotificationHelper.createNotificationChannel(
+ log.d("showNotification[${account.acct.pretty}] creating notification(1)")
+
+ val builder = if (Build.VERSION.SDK_INT >= 26) {
+ // Android 8 から、通知のスタイルはユーザが管理することになった
+ // NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる
+ val channel = NotificationHelper.createNotificationChannel(
context,
account,
trackingName
)
- NotificationCompat.Builder(context, channel.id)
- } else {
- NotificationCompat.Builder(context, "not_used")
- }
-
- builder.apply {
-
- val params = listOf(
+ NotificationCompat.Builder(context, channel.id)
+ } else {
+ NotificationCompat.Builder(context, "not_used")
+ }
+
+ builder.apply {
+
+ val params = listOf(
"db_id" to account.db_id.toString(),
"type" to trackingType.str,
"notificationId" to notificationId
).mapNotNull {
- val second = it.second
- if(second == null) {
- null
- } else {
- "${it.first.encodePercent()}=${second.encodePercent()}"
- }
- }.joinToString("&")
-
- setContentIntent(
+ val second = it.second
+ if (second == null) {
+ null
+ } else {
+ "${it.first.encodePercent()}=${second.encodePercent()}"
+ }
+ }.joinToString("&")
+
+ setContentIntent(
PendingIntent.getActivity(
context,
257,
Intent(context, ActCallback::class.java).apply {
data =
"subwaytooter://notification_click/?$params".toUri()
-
+
// FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
},
PendingIntent.FLAG_UPDATE_CURRENT
)
)
-
- setDeleteIntent(
+
+ setDeleteIntent(
PendingIntent.getBroadcast(
context,
257,
@@ -1523,170 +1520,170 @@ class PollingWorker private constructor(contextArg : Context) {
PendingIntent.FLAG_UPDATE_CURRENT
)
)
-
- setAutoCancel(true)
-
- // 常に白テーマのアイコンを使う
- setSmallIcon(R.drawable.ic_notification)
-
- // 常に白テーマの色を使う
- builder.color = ContextCompat.getColor(context, R.color.Light_colorAccent)
-
- // Android 7.0 ではグループを指定しないと勝手に通知が束ねられてしまう。
- // 束ねられた通知をタップしても pi_click が実行されないので困るため、
- // アカウント別にグループキーを設定する
- setGroup(context.packageName + ":" + account.acct.ascii)
-
- }
-
- log.d("showNotification[${account.acct.pretty}] creating notification(3)")
-
- setContent(builder)
-
- log.d("showNotification[${account.acct.pretty}] set notification...")
-
- notification_manager.notify(notification_tag, NOTIFICATION_ID, builder.build())
- }
- }
- }
- }
-
- private fun getNotificationLine(item : Data) : String {
- val name = when(Pref.bpShowAcctInSystemNotification(pref)) {
+
+ setAutoCancel(true)
+
+ // 常に白テーマのアイコンを使う
+ setSmallIcon(R.drawable.ic_notification)
+
+ // 常に白テーマの色を使う
+ builder.color = ContextCompat.getColor(context, R.color.Light_colorAccent)
+
+ // Android 7.0 ではグループを指定しないと勝手に通知が束ねられてしまう。
+ // 束ねられた通知をタップしても pi_click が実行されないので困るため、
+ // アカウント別にグループキーを設定する
+ setGroup(context.packageName + ":" + account.acct.ascii)
+
+ }
+
+ log.d("showNotification[${account.acct.pretty}] creating notification(3)")
+
+ setContent(builder)
+
+ log.d("showNotification[${account.acct.pretty}] set notification...")
+
+ notification_manager.notify(notification_tag, NOTIFICATION_ID, builder.build())
+ }
+ }
+ }
+ }
+
+ private fun getNotificationLine(item: Data): String {
+ val name = when (Pref.bpShowAcctInSystemNotification(pref)) {
false -> item.notification.accountRef?.decoded_display_name
-
+
true -> {
val acctPretty = item.notification.accountRef?.get()?.acct?.pretty
- if(acctPretty?.isNotEmpty() == true) {
+ if (acctPretty?.isNotEmpty() == true) {
"@$acctPretty"
} else {
null
}
}
- } ?: "?"
- return when(item.notification.type) {
+ } ?: "?"
+ return when (item.notification.type) {
TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY ->
"- " + context.getString(R.string.display_name_replied_by, name)
-
+
TootNotification.TYPE_RENOTE,
TootNotification.TYPE_REBLOG ->
"- " + context.getString(R.string.display_name_boosted_by, name)
-
+
TootNotification.TYPE_QUOTE ->
"- " + context.getString(R.string.display_name_quoted_by, name)
- TootNotification.TYPE_STATUS->
+ TootNotification.TYPE_STATUS ->
"- " + context.getString(R.string.display_name_posted_by, name)
-
+
TootNotification.TYPE_FOLLOW ->
"- " + context.getString(R.string.display_name_followed_by, name)
-
+
TootNotification.TYPE_UNFOLLOW ->
"- " + context.getString(R.string.display_name_unfollowed_by, name)
-
+
TootNotification.TYPE_FAVOURITE ->
"- " + context.getString(R.string.display_name_favourited_by, name)
-
+
TootNotification.TYPE_REACTION ->
"- " + context.getString(R.string.display_name_reaction_by, name)
-
+
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL_VOTE_MISSKEY ->
"- " + context.getString(R.string.display_name_voted_by, name)
-
+
TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY ->
"- " + context.getString(R.string.display_name_follow_request_by, name)
-
+
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY ->
"- " + context.getString(R.string.display_name_follow_request_accepted_by, name)
-
+
TootNotification.TYPE_POLL ->
"- " + context.getString(R.string.end_of_polling_from, name)
-
- else -> "- " + "?"
- }
- }
-
- private fun processInjectedData() {
- while(true) {
- val data = inject_queue.poll() ?: break
- val account = SavedAccount.loadAccount(context, data.account_db_id) ?: continue
- val list = data.list
- NotificationCache(data.account_db_id).apply {
- load()
- inject(account, list)
- }
- }
- }
-
- private fun deleteCacheData(db_id : Long) {
- SavedAccount.loadAccount(context, db_id) ?: return
- NotificationCache.deleteCache(db_id)
- }
-
- private fun createErrorNotification(error_instance : ArrayList) {
- if(error_instance.isEmpty()) {
- return
- }
-
- // 通知タップ時のPendingIntent
- val intent_click = Intent(context, ActCallback::class.java)
- // FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない
- intent_click.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- val pi_click = PendingIntent.getActivity(
+
+ else -> "- " + "?"
+ }
+ }
+
+ private fun processInjectedData() {
+ while (true) {
+ val data = inject_queue.poll() ?: break
+ val account = SavedAccount.loadAccount(context, data.account_db_id) ?: continue
+ val list = data.list
+ NotificationCache(data.account_db_id).apply {
+ load()
+ inject(account, list)
+ }
+ }
+ }
+
+ private fun deleteCacheData(db_id: Long) {
+ SavedAccount.loadAccount(context, db_id) ?: return
+ NotificationCache.deleteCache(db_id)
+ }
+
+ private fun createErrorNotification(error_instance: ArrayList) {
+ if (error_instance.isEmpty()) {
+ return
+ }
+
+ // 通知タップ時のPendingIntent
+ val intent_click = Intent(context, ActCallback::class.java)
+ // FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない
+ intent_click.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ val pi_click = PendingIntent.getActivity(
context,
3,
intent_click,
PendingIntent.FLAG_UPDATE_CURRENT
)
-
- val builder = if(Build.VERSION.SDK_INT >= 26) {
- // Android 8 から、通知のスタイルはユーザが管理することになった
- // NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる
- val channel = NotificationHelper.createNotificationChannel(
+
+ val builder = if (Build.VERSION.SDK_INT >= 26) {
+ // Android 8 から、通知のスタイルはユーザが管理することになった
+ // NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる
+ val channel = NotificationHelper.createNotificationChannel(
context,
"ErrorNotification",
"Error",
null,
2 /* NotificationManager.IMPORTANCE_LOW */
)
- NotificationCompat.Builder(context, channel.id)
- } else {
- NotificationCompat.Builder(context, "not_used")
- }
-
- builder
- .setContentIntent(pi_click)
- .setAutoCancel(true)
- .setSmallIcon(R.drawable.ic_notification) // ここは常に白テーマのアイコンを使う
- .setColor(
+ NotificationCompat.Builder(context, channel.id)
+ } else {
+ NotificationCompat.Builder(context, "not_used")
+ }
+
+ builder
+ .setContentIntent(pi_click)
+ .setAutoCancel(true)
+ .setSmallIcon(R.drawable.ic_notification) // ここは常に白テーマのアイコンを使う
+ .setColor(
ContextCompat.getColor(
context,
R.color.Light_colorAccent
)
) // ここは常に白テーマの色を使う
- .setWhen(System.currentTimeMillis())
- .setGroup(context.packageName + ":" + "Error")
-
- run {
- val header = context.getString(R.string.error_notification_title)
- val summary = context.getString(R.string.error_notification_summary)
-
- builder
- .setContentTitle(header)
- .setContentText(summary + ": " + error_instance[0])
-
- val style = NotificationCompat.InboxStyle()
- .setBigContentTitle(header)
- .setSummaryText(summary)
- for(i in 0 .. 4) {
- if(i >= error_instance.size) break
- style.addLine(error_instance[i])
- }
- builder.setStyle(style)
- }
- notification_manager.notify(NOTIFICATION_ID_ERROR, builder.build())
- }
+ .setWhen(System.currentTimeMillis())
+ .setGroup(context.packageName + ":" + "Error")
+
+ run {
+ val header = context.getString(R.string.error_notification_title)
+ val summary = context.getString(R.string.error_notification_summary)
+
+ builder
+ .setContentTitle(header)
+ .setContentText(summary + ": " + error_instance[0])
+
+ val style = NotificationCompat.InboxStyle()
+ .setBigContentTitle(header)
+ .setSummaryText(summary)
+ for (i in 0..4) {
+ if (i >= error_instance.size) break
+ style.addLine(error_instance[i])
+ }
+ builder.setStyle(style)
+ }
+ notification_manager.notify(NOTIFICATION_ID_ERROR, builder.build())
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ViewHolderHeaderProfile.kt b/app/src/main/java/jp/juggler/subwaytooter/ViewHolderHeaderProfile.kt
index 3916e1f1..c30b387c 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ViewHolderHeaderProfile.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ViewHolderHeaderProfile.kt
@@ -2,6 +2,7 @@ package jp.juggler.subwaytooter
import android.app.Dialog
import android.graphics.Color
+import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ForegroundColorSpan
@@ -11,19 +12,16 @@ import jp.juggler.emoji.EmojiMap
import jp.juggler.subwaytooter.action.Action_Follow
import jp.juggler.subwaytooter.action.Action_User
import jp.juggler.subwaytooter.api.*
-import jp.juggler.subwaytooter.api.entity.TootAccount
-import jp.juggler.subwaytooter.api.entity.TootAccountRef
-import jp.juggler.subwaytooter.api.entity.TootStatus
+import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.span.EmojiImageSpan
+import jp.juggler.subwaytooter.span.LinkInfo
+import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.span.createSpan
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
-import jp.juggler.subwaytooter.util.DecodeOptions
-import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator
-import jp.juggler.subwaytooter.util.openCustomTab
-import jp.juggler.subwaytooter.util.startMargin
+import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.MyLinkMovementMethod
import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.subwaytooter.view.MyTextView
@@ -38,6 +36,8 @@ internal class ViewHolderHeaderProfile(
private val ivBackground : MyNetworkImageView
private val tvCreated : TextView
private val tvLastStatusAt : TextView
+ private val tvFeaturedTags : TextView
+
private val ivAvatar : MyNetworkImageView
private val tvDisplayName : TextView
private val tvAcct : TextView
@@ -79,6 +79,7 @@ internal class ViewHolderHeaderProfile(
llProfile = viewRoot.findViewById(R.id.llProfile)
tvCreated = viewRoot.findViewById(R.id.tvCreated)
tvLastStatusAt = viewRoot.findViewById(R.id.tvLastStatusAt)
+ tvFeaturedTags = viewRoot.findViewById(R.id.tvFeaturedTags)
ivAvatar = viewRoot.findViewById(R.id.ivAvatar)
tvDisplayName = viewRoot.findViewById(R.id.tvDisplayName)
tvAcct = viewRoot.findViewById(R.id.tvAcct)
@@ -160,6 +161,7 @@ internal class ViewHolderHeaderProfile(
tvMovedName.textSize = f
tvMoved.textSize = f
tvPersonalNotes.textSize = f
+ tvFeaturedTags.textSize = f
}
f = activity.acct_font_size_sp
@@ -187,7 +189,8 @@ internal class ViewHolderHeaderProfile(
btnStatusCount.textColor = contentColor
btnFollowing.textColor = contentColor
btnFollowers.textColor = contentColor
-
+ tvFeaturedTags.textColor = contentColor
+
setIconDrawableId(
activity,
btnMore,
@@ -208,6 +211,7 @@ internal class ViewHolderHeaderProfile(
tvCreated.textColor = acctColor
tvMovedAcct.textColor = acctColor
tvLastStatusAt.textColor = acctColor
+
val whoRef = column.who_account
this.whoRef = whoRef
@@ -231,6 +235,7 @@ internal class ViewHolderHeaderProfile(
relation = null
tvCreated.text = ""
tvLastStatusAt.vg(false)
+ tvFeaturedTags.vg(false)
ivBackground.setImageDrawable(null)
ivAvatar.setImageDrawable(null)
@@ -259,6 +264,28 @@ internal class ViewHolderHeaderProfile(
invalidator = null,
fromProfileHeader = true
)
+
+ val featuredTagsText = column.who_featured_tags?.notEmpty()?.let{ tagList->
+ SpannableStringBuilder().apply {
+ append(activity.getString(R.string.featured_hashtags))
+ append(":")
+ tagList.forEach { tag ->
+ append(" ")
+ val tagWithSharp = "#" + tag.name
+ val start = length
+ append(tagWithSharp)
+ val end = length
+ tag.url?.notEmpty()?.let{ url->
+ val span = MyClickableSpan(LinkInfo(url = url, tag = tag.name, caption = tagWithSharp))
+ setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+ }
+ }
+ }
+ tvFeaturedTags.vg( featuredTagsText !=null)?.let{
+ it.text = featuredTagsText!!
+ it.movementMethod = MyLinkMovementMethod
+ }
ivBackground.setImageUrl(
activity.pref,
diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootParser.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootParser.kt
index b0398184..e8f3e0a1 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/api/TootParser.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootParser.kt
@@ -24,10 +24,10 @@ class TootParser(
val misskeyUserRelationMap = HashMap()
// val misskeyAccountDetailMap = HashMap()
- val apiHost : Host?
+ val apiHost : Host
get() = linkHelper.apiHost
- val apDomain : Host?
+ val apDomain : Host
get() = linkHelper.apDomain
init {
diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt
index 2dff5e01..39cf11a3 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt
@@ -127,9 +127,10 @@ open class TootAccount(parser : TootParser, src : JsonObject) : HostAndDomain {
init {
this.json = src
src["_fromStream"] = parser.fromStream
-
+
if(parser.serviceType == ServiceType.MISSKEY) {
-
+
+
this.custom_emojis =
parseMapOrNull(CustomEmoji.decodeMisskey, src.jsonArray("emojis"))
this.profile_emojis = null
diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootTag.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootTag.kt
index f35e3f08..059b3579 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootTag.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootTag.kt
@@ -7,181 +7,187 @@ import jp.juggler.util.*
import java.util.regex.Pattern
open class TootTag constructor(
-
+
// The hashtag, not including the preceding #
- val name : String,
-
+ val name: String,
+
// The URL of the hashtag. may null if generated from TootContext
- val url : String? = null,
-
+ val url: String? = null,
+
// Mastodon /api/v2/search provides history.
- val history : ArrayList? = null
+ val history: ArrayList? = null
) : TimelineItem() {
-
- val countDaily : Int
- val countWeekly : Int
- val accountDaily : Int
- val accountWeekly : Int
-
- init {
- countDaily = history?.first()?.uses ?: 0
- countWeekly = history?.sumBy { it.uses } ?: 0
-
- accountDaily = history?.first()?.accounts ?: 0
- accountWeekly = history?.map { it.accounts }?.maxOrNull() ?: accountDaily
- }
-
- class History(src : JsonObject) {
-
- private val day : Long
- val uses : Int
- val accounts : Int
-
- init {
- day = src.long("day")
- ?: throw RuntimeException("TootTrendTag.History: missing day")
- uses = src.int("uses")
- ?: throw RuntimeException("TootTrendTag.History: missing uses")
- accounts = src.int("accounts")
- ?: throw RuntimeException("TootTrendTag.History: missing accounts")
- }
-
- }
-
- companion object {
-
- val log = LogCategory("TootTag")
-
- fun parse(parser : TootParser, src : JsonObject) =
- if(parser.linkHelper.isMisskey) {
- val name = src.stringOrThrow("tag")
- val url = "https://${parser.apiHost}/tags/${Uri.encode(name)}"
- TootTag(
+
+ val countDaily: Int
+ val countWeekly: Int
+ val accountDaily: Int
+ val accountWeekly: Int
+
+ init {
+ countDaily = history?.first()?.uses ?: 0
+ countWeekly = history?.sumBy { it.uses } ?: 0
+
+ accountDaily = history?.first()?.accounts ?: 0
+ accountWeekly = history?.map { it.accounts }?.maxOrNull() ?: accountDaily
+ }
+
+ class History(src: JsonObject) {
+
+ private val day: Long
+ val uses: Int
+ val accounts: Int
+
+ init {
+ day = src.long("day")
+ ?: throw RuntimeException("TootTrendTag.History: missing day")
+ uses = src.int("uses")
+ ?: throw RuntimeException("TootTrendTag.History: missing uses")
+ accounts = src.int("accounts")
+ ?: throw RuntimeException("TootTrendTag.History: missing accounts")
+ }
+
+ }
+
+ companion object {
+
+ val log = LogCategory("TootTag")
+
+ private val reHeadSharp = """\A#""".toRegex()
+
+ private val reUserTagUrl = """/@[^/]+/tagged/""".toRegex()
+
+ fun parse(parser: TootParser, src: JsonObject) =
+ if (parser.linkHelper.isMisskey) {
+ val name = src.stringOrThrow("tag").replaceFirst(reHeadSharp, "")
+ TootTag(
name = name,
- url = url,
- history = null
+ url = "https://${parser.apiHost}/tags/${Uri.encode(name)}"
)
- } else {
- TootTag(
- name = src.stringOrThrow("name"),
- url = src.string("url"),
+ } else {
+ // /api/v1/accounts/$id/featured_tags の場合、
+ // name部分は先頭に#がついているかもしれない。必要なら除去するべき。
+ // url部分はユーザのタグTLになる。 https://nightly.fedibird.com/@noellabo/tagged/fedibird
+ // STはメニューから選べるので、URLは普通のタグURLの方が望ましい https://nightly.fedibird.com/tags/fedibird
+ TootTag(
+ name = src.stringOrThrow("name").replaceFirst(reHeadSharp, ""),
+ url = src.string("url")?.replaceFirst(reUserTagUrl, "/tags/"),
history = parseHistories(src.jsonArray("history"))
)
- }
-
- private fun parseHistories(src : JsonArray?) : ArrayList? {
- src ?: return null
-
- val dst = ArrayList()
- src.objectList().forEach {
- try {
- dst.add(History(it))
- } catch(ex : Throwable) {
- log.e(ex, "parseHistories failed.")
- }
- }
- return dst
- }
-
- fun parseListOrNull(parser : TootParser, array : JsonArray?) =
- array?.mapNotNull { src->
- try {
- when(src) {
+ }
+
+ private fun parseHistories(src: JsonArray?): ArrayList? {
+ src ?: return null
+
+ val dst = ArrayList()
+ src.objectList().forEach {
+ try {
+ dst.add(History(it))
+ } catch (ex: Throwable) {
+ log.e(ex, "parseHistories failed.")
+ }
+ }
+ return dst
+ }
+
+ fun parseListOrNull(parser: TootParser, array: JsonArray?) =
+ array?.mapNotNull { src ->
+ try {
+ when (src) {
null -> null
"" -> null
is String -> TootTag(name = src)
is JsonObject -> parse(parser, src)
- else->null
- }
- }catch(ex:Throwable){
- log.e(ex,"parseListOrNull failed.")
- null
- }
- }?.notEmpty()
-
- fun parseList(parser : TootParser, array : JsonArray?) =
- parseListOrNull(parser,array) ?: emptyList()
-
- private const val w = TootAccount.reRubyWord
- private const val a = TootAccount.reRubyAlpha
- private const val s = "_\\u00B7\\u200c" // separators
-
- private fun generateMastodonTagPattern() : Pattern {
- val reMastodonTagName = """([_$w][$s$w]*[$s$a][$s$w]*[_$w])|([_$w]*[$a][_$w]*)"""
- return """(?:^|[^\w/)])#($reMastodonTagName)""".asciiPattern()
- }
-
- private val reMastodonTag = generateMastodonTagPattern()
-
- // https://medium.com/@alice/some-article#.abcdef123 => タグにならない
- // https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit => タグにならない
- // #aesthetic => #aesthetic
- // #3d => #3d
- // #l33ts35k => #l33ts35k
- // #world2016 => #world2016
- // #_test => #_test
- // #test_ => #test_
- // #one·two·three· => 末尾の・はタグに含まれない。#one·two·three までがハッシュタグになる。
- // #0123456' => 数字だけのハッシュタグはタグとして認識されない。
- // #000_000 => 認識される。orの前半分が機能してるらしい
- //
-
- // タグに使えない文字
- // 入力補完用なのでやや緩め
- private val reCharsNotTagMastodon = """[^$s$w$a]""".asciiPattern()
- private val reCharsNotTagMisskey = """[\s.,!?'${'"'}:/\[\]【】]""".asciiPattern()
-
- // find hashtags in content text(raw)
- // returns null if hashtags not found, or ArrayList of String (tag without #)
- fun findHashtags(src : String, isMisskey : Boolean) : ArrayList? =
- if(isMisskey) {
- MisskeyMarkdownDecoder.findHashtags(src)
- } else {
- var result : ArrayList? = null
- val m = reMastodonTag.matcher(src)
- while(m.find()) {
- if(result == null) result = ArrayList()
- result.add(m.groupEx(1) !!)
- }
- result
- }
-
- fun isValid(src : String, isMisskey : Boolean) =
- if(isMisskey) {
- ! reCharsNotTagMisskey.matcher(src).find()
- } else {
- ! reCharsNotTagMastodon.matcher(src).find()
- }
-
- // https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
- // あるサービスは /tags/... でなく /tag/... を使う
- private val reUrlHashTag = """\Ahttps://([^/]+)/tags?/([^?#・\s\-+.,:;/]+)(?:\z|[?#])"""
- .asciiPattern()
-
- // https://pixelfed.tokyo/discover/tags/SubwayTooter?src=hash
- private val reUrlHashTagPixelfed =
- """\Ahttps://([^/]+)/discover/tags/([^?#・\s\-+.,:;/]+)(?:\z|[?#])"""
- .asciiPattern()
-
- // returns null or pair of ( decoded tag without sharp, host)
- fun String.findHashtagFromUrl() : Pair? {
- var m = reUrlHashTag.matcher(this)
- if(m.find()) {
- val host = m.groupEx(1) !!
- val tag = m.groupEx(2) !!.decodePercent()
- return Pair(tag, host)
- }
-
- m = reUrlHashTagPixelfed.matcher(this)
- if(m.find()) {
- val host = m.groupEx(1) !!
- val tag = m.groupEx(2) !!.decodePercent()
- return Pair(tag, host)
- }
-
- return null
- }
-
- }
-}
\ No newline at end of file
+ else -> null
+ }
+ } catch (ex: Throwable) {
+ log.e(ex, "parseListOrNull failed.")
+ null
+ }
+ }?.notEmpty()
+
+ fun parseList(parser: TootParser, array: JsonArray?) =
+ parseListOrNull(parser, array) ?: emptyList()
+
+
+ private const val w = TootAccount.reRubyWord
+ private const val a = TootAccount.reRubyAlpha
+ private const val s = "_\\u00B7\\u200c" // separators
+
+ private fun generateMastodonTagPattern(): Pattern {
+ val reMastodonTagName = """([_$w][$s$w]*[$s$a][$s$w]*[_$w])|([_$w]*[$a][_$w]*)"""
+ return """(?:^|[^\w/)])#($reMastodonTagName)""".asciiPattern()
+ }
+
+ private val reMastodonTag = generateMastodonTagPattern()
+
+ // https://medium.com/@alice/some-article#.abcdef123 => タグにならない
+ // https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit => タグにならない
+ // #aesthetic => #aesthetic
+ // #3d => #3d
+ // #l33ts35k => #l33ts35k
+ // #world2016 => #world2016
+ // #_test => #_test
+ // #test_ => #test_
+ // #one·two·three· => 末尾の・はタグに含まれない。#one·two·three までがハッシュタグになる。
+ // #0123456' => 数字だけのハッシュタグはタグとして認識されない。
+ // #000_000 => 認識される。orの前半分が機能してるらしい
+ //
+
+ // タグに使えない文字
+ // 入力補完用なのでやや緩め
+ private val reCharsNotTagMastodon = """[^$s$w$a]""".asciiPattern()
+ private val reCharsNotTagMisskey = """[\s.,!?'${'"'}:/\[\]【】]""".asciiPattern()
+
+ // find hashtags in content text(raw)
+ // returns null if hashtags not found, or ArrayList of String (tag without #)
+ fun findHashtags(src: String, isMisskey: Boolean): ArrayList? =
+ if (isMisskey) {
+ MisskeyMarkdownDecoder.findHashtags(src)
+ } else {
+ var result: ArrayList? = null
+ val m = reMastodonTag.matcher(src)
+ while (m.find()) {
+ if (result == null) result = ArrayList()
+ result.add(m.groupEx(1)!!)
+ }
+ result
+ }
+
+ fun isValid(src: String, isMisskey: Boolean) =
+ if (isMisskey) {
+ !reCharsNotTagMisskey.matcher(src).find()
+ } else {
+ !reCharsNotTagMastodon.matcher(src).find()
+ }
+
+ // https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
+ // あるサービスは /tags/... でなく /tag/... を使う
+ private val reUrlHashTag = """\Ahttps://([^/]+)/tags?/([^?#・\s\-+.,:;/]+)(?:\z|[?#])"""
+ .asciiPattern()
+
+ // https://pixelfed.tokyo/discover/tags/SubwayTooter?src=hash
+ private val reUrlHashTagPixelfed =
+ """\Ahttps://([^/]+)/discover/tags/([^?#・\s\-+.,:;/]+)(?:\z|[?#])"""
+ .asciiPattern()
+
+ // returns null or pair of ( decoded tag without sharp, host)
+ fun String.findHashtagFromUrl(): Pair? {
+ var m = reUrlHashTag.matcher(this)
+ if (m.find()) {
+ val host = m.groupEx(1)!!
+ val tag = m.groupEx(2)!!.decodePercent()
+ return Pair(tag, host)
+ }
+
+ m = reUrlHashTagPixelfed.matcher(this)
+ if (m.find()) {
+ val host = m.groupEx(1)!!
+ val tag = m.groupEx(2)!!.decodePercent()
+ return Pair(tag, host)
+ }
+
+ return null
+ }
+ }
+}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PushSubscriptionHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PushSubscriptionHelper.kt
index ea4dcc19..64bf48a1 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/util/PushSubscriptionHelper.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/PushSubscriptionHelper.kt
@@ -150,11 +150,11 @@ class PushSubscriptionHelper(
}
}
- private fun updateSubscriptionMisskey(client: TootApiClient): TootApiResult? {
+ private suspend fun updateSubscriptionMisskey(client: TootApiClient): TootApiResult? {
// 現在の購読状態を取得できないので、毎回購読の更新を行う
// FCMのデバイスIDを取得
- val device_id = PollingWorker.getDeviceId(context)
+ val device_id = PollingWorker.getFirebaseMessagingToken(context)
?: return TootApiResult(error = context.getString(R.string.missing_fcm_device_id))
// アクセストークン
@@ -221,7 +221,7 @@ class PushSubscriptionHelper(
}
}
- private fun updateSubscriptionMastodon(client: TootApiClient, force: Boolean): TootApiResult? {
+ private suspend fun updateSubscriptionMastodon(client: TootApiClient, force: Boolean): TootApiResult? {
// 現在の購読状態を取得
// https://github.com/tootsuite/mastodon/pull/7471
@@ -293,7 +293,7 @@ class PushSubscriptionHelper(
}
// FCMのデバイスIDを取得
- val device_id = PollingWorker.getDeviceId(context)
+ val device_id = PollingWorker.getFirebaseMessagingToken(context)
?: return TootApiResult(error = context.getString(R.string.missing_fcm_device_id))
// アクセストークン
@@ -508,7 +508,7 @@ class PushSubscriptionHelper(
}
}
- fun updateSubscription(client: TootApiClient, force: Boolean = false): TootApiResult? =
+ suspend fun updateSubscription(client: TootApiClient, force: Boolean = false): TootApiResult? =
try {
when {
isRecentlyChecked() ->
diff --git a/app/src/main/res/layout/lv_header_profile.xml b/app/src/main/res/layout/lv_header_profile.xml
index 5689c255..0d17bf70 100644
--- a/app/src/main/res/layout/lv_header_profile.xml
+++ b/app/src/main/res/layout/lv_header_profile.xml
@@ -1,6 +1,5 @@
-
+ tools:text="aaa\@bbb さんは引っ越しました" />
+ android:orientation="horizontal">
+ android:orientation="vertical">
+ tools:text="Follower Name" />
+ tools:text="aaaaaaaaaaaaaaaa" />
+ android:layout_marginStart="4dp">
+ tools:src="@drawable/ic_follow_plus" />
+ tools:ignore="ContentDescription" />
@@ -104,20 +95,17 @@
android:layout_height="wrap_content"
android:gravity="end"
android:textSize="12sp"
- tools:text="xxxx-xx-xx xx:xx:xx"
- />
+ tools:text="xxxx-xx-xx xx:xx:xx" />
+ android:layout_height="wrap_content">
+ android:scaleType="centerCrop" />
+ android:padding="12dp">
+ android:background="@drawable/btn_bg_transparent_round6dp" />
+ tools:text="ディスプレイネームディスプレイネームディスプレイネーム" />
+ tools:text="\@fugahogehogera\@jugemujyugemugokounosurikire.jp" />
+ tools:text="説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 " />
+ tools:text="説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 説明文 " />
+ android:layout_marginTop="12dp">
+ tools:src="@drawable/ic_follow_plus" />
+ tools:ignore="ContentDescription" />
+ android:orientation="vertical" />
+
+
+
+ tools:text="xxxx-xx-xx xx:xx:xx" />
+ android:text="@string/personal_notes" />
+ android:orientation="horizontal">
+
+ android:text="@string/personal_notes" />
+
+ android:src="@drawable/ic_edit" />
+ android:text="@string/remote_profile_warning" />
+ android:orientation="horizontal">
+ tools:text="statuses\n124" />
+ tools:text="following\n9999" />
+ tools:text="followers\n9999" />
+ android:paddingEnd="4dp"
+ android:src="@drawable/ic_more" />
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 7d15b932..e95be25b 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -634,7 +634,7 @@
Mostra (mitjançant) el nom de l\'aplicació, si és possible
Forçar que s\'afegeixi una separació en refrescar la part superior
Mida d\'icona d\'avatar (Unitat:dp. per defecte:48. cal reiniciar aplicació)
- Quocient d\'arrodoniment de la icona d\'avatar (Unitat:%. per defrecte:33. cal reiniciar aplicació)
+ Quocient d\'arrodoniment de la icona d\'avatar (Unitat:%%. per defrecte:33. cal reiniciar aplicació)
No arrodonir els angles de la icona d\'avatar (cal reiniciar l\'aplicació)
Comparteix la visualització entre pissarres
Rebateja…
@@ -821,7 +821,7 @@
A:
Fer ús de \"Nota citada\"
Vols silenciar l\'aplicació \"%1$s\"\?
- Augmenta l\'opacitat del botó de difusió (Unitat:%. cal reiniciar aplicació. també afecta al text de contingut alfa.)
+ Augmenta l\'opacitat del botó de difusió (Unitat:%%. cal reiniciar aplicació. també afecta al text de contingut alfa.)
Color de fons dels brams
Color per defecte de les pissarres
Color de fons de la capçalera
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 1908c949..86a7c4d6 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -1,5 +1,5 @@
-
+
Subway Tooter
Einstellungen
Konto hinzufügen
@@ -321,7 +321,7 @@
Lese Kopfspalt
Nur entfernt
Promotet
- Ereignis-Zeitraum: %1$s...%2$s
+ Ereignis-Zeitraum: %1$s…%2$s
Ereignis-Zeitraum: %1$s
Sichtbar boosten
Verberge die Summe bis zum Ablauf
@@ -424,7 +424,7 @@
Nennung
Teile Anzeigenpool zwischen den Spalten
Erlaube Verdoppelung von Spalten
- Abrundungsfaktor des Avatarbildes (Einheit: %. Standard: 33. App-Neustart erforderlich)
+ Abrundungsfaktor des Avatarbildes (Einheit: %%. Standard: 33. App-Neustart erforderlich)
Zeige per App-Name, falls möglich
Kann Domänen auf deinem Server nicht blockieren.
Kann Domäne nicht vom Pseudokonto aus blockieren.
@@ -569,11 +569,11 @@
In deinem Profil promoten
Bereits abgestimmt.
Hintergrundfarbe des Toots
- Deckkraft des Boost-Buttons (Einheit: %. App-Neustart erforderlich; wird auch von der Transparenz des Inhaltstextes beeinflusst)
+ Deckkraft des Boost-Buttons (Einheit: %%. App-Neustart erforderlich; wird auch von der Transparenz des Inhaltstextes beeinflusst)
App \"%1$s\" stummschalten\?
Benutze “Toot zitieren”. (Misskey oder individiualisierter Mastodonserver)
An:
- ...und weitere
+ …und weitere
Teilnehmer:
Unterhaltung anzeigen
Das Feature \"Timeline um die angegebene Zeit herum\" ist auf Mastodonserver ab Version 2.6.0 beschränkt. Eine etwas neuere Timeline kann nicht abgerufen werden.
@@ -982,7 +982,7 @@
Zeige „Schnell-Toot“-Leiste (App-Neustart erforderlich)
Schriftgröße der Timeline
\n(Einheit: Skalierbare Pixel / SP. Leer lassen, um Standard zu benutzen. App-Neustart erforderlich)
-\n...
+\n…
\n…
Benachrichtungston
Timeline von \"%1$s\" öffnen
@@ -1007,7 +1007,7 @@
Blockierte Domains
(URL ausgelassen)
Aktiviert Sprachausgabe
- Konnte Konto nicht wechseln. Alte Entwurfsdaten besitzen kein in_reply_to_url, kann für gewählten Server nicht in in_reply_to_url umgewandelt werden.
+ Konnte Konto nicht wechseln. Alte Entwurfsdaten besitzen kein in_reply_to_url, kann für gewählten Server nicht in in_reply_to_url umgewandelt werden.
Standardkonto, wenn der „Toot“-Button gedrückt wird
Es gibt auch eine Konteneinstellung, um Benachrichtigungen ein- und auszuschalten.
Verhalten
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 0d560278..2daa422e 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -602,7 +602,7 @@
Afficher (via) le nom de l\'application si possible
Forcer l’ajout d\'un espace lors du rafraîchissement du dessus
Taille de l’icône de l’avatar (Unité:dp. par défaut:48. Redémarrage de l’app requis)
- Ratio de l’arrondi de l’icône de l’avatar (Unité:%. défaut:33. redémarrage de l\'application requis)
+ Ratio de l’arrondi de l’icône de l’avatar (Unité:%%. défaut:33. redémarrage de l\'application requis)
Ne pas arrondir les coins de l’avatar (redémarrage requis)
Partager le pool de vues entre les colonnes
Renommer …
@@ -849,7 +849,7 @@
Taille de l’icône de la barre de colonne (Unité : dp. Défaut : 30. Redémarrage de l’application requis)
Afficher le compte (au lieu du nom d’utilisateur) dans les notifications système
Fil public autour …
- Transparence du bouton Boost (Unité:%. redémarrage de l’app nécessaire. Affecte également la transparence du contenu texte.)
+ Transparence du bouton Boost (Unité:%%. redémarrage de l’app nécessaire. Affecte également la transparence du contenu texte.)
Afficher le filtre rapide des notifications dans la configuration de la colonne (redémarrage de l’app requis)
Compte officiel
Sorties
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index cfba7037..a787ddbc 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -72,7 +72,7 @@
(自動CW)
(マストドン2.4以降で利用可能)
ユーザ画像を角丸にしない(アプリ再起動が必要)
- ユーザ画像の角丸率(単位:%。デフォルト:33。アプリ再起動が必要)
+ ユーザ画像の角丸率(単位:%%。デフォルト:33。アプリ再起動が必要)
ユーザ画像の大きさ(単位:dp。デフォルト:48。アプリ再起動が必要)
戻るボタンの動作
戻るボタンでカラム一覧を開く
@@ -809,7 +809,7 @@
送り先:
引用トゥートにする(MisskeyまたはカスタマイズされたMastodonサーバ)
アプリ \"%1$s\" はミュートされます。よろしいですか?
- ブーストボタンのアルファ不透明度(単位:%。アプリの再起動が必要。本文テキスト色のアルファ値の影響も受けます)
+ ブーストボタンのアルファ不透明度(単位:%%。アプリの再起動が必要。本文テキスト色のアルファ値の影響も受けます)
トゥート背景色
\'未収載\' 公開範囲
\'フォロワーのみ\' 公開範囲
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 7327cedf..be27db05 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -611,7 +611,7 @@
가능하면 (경유) 앱 이름 보이기
위로 새로고침할 때 강제로 띄움 넣기
아바타 아이콘 크기 (단위:dp. 기본값:48. 앱 재시작 필요)
- 아바타 아이콘 곡률 (Unit:%. 기본값:33. 앱 재시작 필요)
+ 아바타 아이콘 곡률 (Unit:%%. 기본값:33. 앱 재시작 필요)
아바타 아이콘의 모서리를 둥글게 하지 않기 (앱 재시작 필요)
칼럼 사이에 뷰 풀을 공유
이름 변경…
@@ -796,7 +796,7 @@
받을 이:
\"인용 리노트\" 사용
앱 \"%1$s\"를 음소거할까요\?
- 부스트 버튼 알파 불투명도 (단위:%. 앱 재시작 필요. 본문의 알파에 영향을 미침.)
+ 부스트 버튼 알파 불투명도 (단위:%%. 앱 재시작 필요. 본문의 알파에 영향을 미침.)
툿 배경색
칼럼 색 기본값
헤더 배경색
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index 1e36dcd0..8e5cc462 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -678,7 +678,7 @@
Media har ikke blitt lastet opp enda.
Framhev nøkkelord
Avatarikonstørrelse (enhet:dp. Forvalg:48. Programomstart kreves)
- Avatarikonstørrelseforhold (enhet:%. Forvalg:33. Programomstart kreves)
+ Avatarikonstørrelseforhold (enhet:%%. Forvalg:33. Programomstart kreves)
Ikke avrund avatarikonets hjørner (programomstart kreves)
Tillat duplisering av kolonner
Ikke valgt når systemmerknad trykkes
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 71a14a38..2b5d318d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -621,7 +621,7 @@
Show (via) application name if possible
Force adding gap when refreshing top
Avatar icon size (Unit:dp. default:48. app restart required)
- Avatar icon round ratio (Unit:%. default:33. app restart required)
+ Avatar icon round ratio (Unit:%%. default:33. app restart required)
Don\'t round corner of avatar icon (app restart required)
Share view pool between columns
Rename…
@@ -815,7 +815,7 @@
To:
Use \"Quote Toot\". (Misskey or some customized Mastodon server)
Mute the app \"%1$s\"?
- Boost button alpha opacity (Unit:%. app restart required. also affected of content text alpha.)
+ Boost button alpha opacity (Unit:%%. app restart required. also affected of content text alpha.)
Toot background color
Column color default
Header background color
diff --git a/checkMissingTranslation.pl b/checkMissingTranslation.pl
index 40f111e3..39403854 100644
--- a/checkMissingTranslation.pl
+++ b/checkMissingTranslation.pl
@@ -122,8 +122,8 @@ for my $lang ( sort keys %langs ){
# 残りの部分に%が登場したらエラー
my $sv = $value;
- $sv =~ s/(%\d+\$[\d\.]*[sdxf])//g;
- if( $sv =~ /%/ && not $sv=~/:%/ ){
+ $sv =~ s/(%\d+\$[\d\.]*[sdxf])|%%//g;
+ if( $sv =~ /%/ ){
$hasError =1;
print "!! ($lang)$name : broken param: $sv // $value\n";
}