remove pre-2.4.0 style unsafe notification listener

This commit is contained in:
tateisu 2018-12-07 07:43:13 +09:00
parent 67bb8e899c
commit 365485cf91
14 changed files with 16 additions and 828 deletions

View File

@ -220,11 +220,6 @@
android:label="@string/nickname_and_color_and_notification_sound"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
/>
<activity
android:name=".ActCustomStreamListener"
android:label="@string/custom_stream_listener"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
/>
<activity
android:name=".ActText"

View File

@ -311,8 +311,6 @@ class ActAppSettingChild : AppCompatActivity()
, R.id.btnTimelineFontReset
, R.id.btnTimelineFontBoldEdit
, R.id.btnTimelineFontBoldReset
, R.id.btnCustomStreamListenerEdit
, R.id.btnCustomStreamListenerReset
, R.id.btnCcdHeaderBackgroundEdit
, R.id.btnCcdHeaderBackgroundReset
, R.id.btnCcdHeaderForegroundEdit
@ -909,19 +907,6 @@ class ActAppSettingChild : AppCompatActivity()
} catch(ex : Throwable) {
showToast(this, ex, "could not open picker for font")
}
R.id.btnCustomStreamListenerEdit -> ActCustomStreamListener.open(this)
R.id.btnCustomStreamListenerReset -> {
pref.edit()
.remove(Pref.spStreamListenerConfigUrl)
.remove(Pref.spStreamListenerSecret)
.remove(Pref.spStreamListenerConfigData)
.apply()
SavedAccount.clearRegistrationCache()
PollingWorker.queueUpdateListener(this)
showToast(this, false, getString(R.string.custom_stream_listener_was_reset))
}
}
}

View File

@ -1,364 +0,0 @@
package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.EditText
import android.widget.TextView
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import okhttp3.Request
import org.hjson.JsonValue
import java.util.regex.Pattern
class ActCustomStreamListener : AppCompatActivity(), View.OnClickListener, TextWatcher {
companion object {
internal val log = LogCategory("ActCustomStreamListener")
// internal val EXTRA_ACCT = "acct"
fun open(activity : Activity) {
val intent = Intent(activity, ActCustomStreamListener::class.java)
activity.startActivity(intent)
}
internal const val STATE_STREAM_CONFIG_JSON = "stream_config_json"
internal val reInstanceURL = Pattern.compile("\\Ahttps://[a-z0-9.-_:]+\\z")
internal val reUpperCase = Pattern.compile("[A-Z]")
internal val reUrl = Pattern.compile("\\Ahttps?://[\\w\\-?&#%~!$'()*+,/:;=@._\\[\\]]+\\z")
}
private lateinit var etStreamListenerConfigurationUrl : EditText
private lateinit var etStreamListenerSecret : EditText
private lateinit var tvLog : TextView
private lateinit var btnDiscard : View
private lateinit var btnTest : View
private lateinit var btnSave : View
internal var stream_config_json : String? = null
private var bLoading = false
private val isTestRunning : Boolean
get() = last_task?.isCancelled ?: false
internal var last_task : AsyncTask<Void, Void, String?>? = null
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
App1.setActivityTheme(this, false)
initUI()
if(savedInstanceState != null) {
stream_config_json = savedInstanceState.getString(STATE_STREAM_CONFIG_JSON)
} else {
load()
}
showButtonState()
}
override fun onSaveInstanceState(outState : Bundle?) {
super.onSaveInstanceState(outState)
outState ?: return
outState.putString(STATE_STREAM_CONFIG_JSON, stream_config_json)
}
private fun initUI() {
setContentView(R.layout.act_custom_stream_listener)
Styler.fixHorizontalPadding(findViewById(R.id.llContent))
etStreamListenerConfigurationUrl = findViewById(R.id.etStreamListenerConfigurationUrl)
etStreamListenerSecret = findViewById(R.id.etStreamListenerSecret)
etStreamListenerConfigurationUrl.addTextChangedListener(this)
etStreamListenerSecret.addTextChangedListener(this)
tvLog = findViewById(R.id.tvLog)
btnDiscard = findViewById(R.id.btnDiscard)
btnTest = findViewById(R.id.btnTest)
btnSave = findViewById(R.id.btnSave)
btnDiscard.setOnClickListener(this)
btnTest.setOnClickListener(this)
btnSave.setOnClickListener(this)
}
private fun load() {
bLoading = true
val pref = Pref.pref(this)
etStreamListenerConfigurationUrl.setText(Pref.spStreamListenerConfigUrl(pref))
etStreamListenerSecret.setText(Pref.spStreamListenerSecret(pref))
stream_config_json = null
tvLog.text = getString(R.string.input_url_and_secret_then_test)
bLoading = false
}
override fun beforeTextChanged(s : CharSequence, start : Int, count : Int, after : Int) {
}
override fun onTextChanged(s : CharSequence, start : Int, before : Int, count : Int) {
}
override fun afterTextChanged(s : Editable) {
tvLog.text = getString(R.string.input_url_and_secret_then_test)
stream_config_json = null
showButtonState()
}
private fun showButtonState() {
btnSave.isEnabled = stream_config_json != null
btnTest.isEnabled = ! isTestRunning
}
override fun onClick(v : View) {
when(v.id) {
R.id.btnDiscard -> {
etStreamListenerConfigurationUrl.hideKeyboard()
finish()
}
R.id.btnTest -> {
etStreamListenerConfigurationUrl.hideKeyboard()
startTest()
}
R.id.btnSave -> {
etStreamListenerConfigurationUrl.hideKeyboard()
if(save()) {
SavedAccount.clearRegistrationCache()
PollingWorker.queueUpdateListener(this)
finish()
}
}
}
}
private fun save() : Boolean {
if(stream_config_json == null) {
showToast(this, false, "please test before save.")
return false
}
Pref.pref(this).edit()
.put(
Pref.spStreamListenerConfigUrl,
etStreamListenerConfigurationUrl.text.toString().trim { it <= ' ' })
.put(
Pref.spStreamListenerSecret,
etStreamListenerSecret.text.toString().trim { it <= ' ' })
.put(Pref.spStreamListenerConfigData, stream_config_json ?: "")
.apply()
return true
}
internal fun addLog(line : String) {
runOnMainLooper {
val old = tvLog.text.toString()
tvLog.text = if(old.isEmpty()) line else old + "\n" + line
}
}
@SuppressLint("StaticFieldLeak")
private fun startTest() {
val strSecret = etStreamListenerSecret.text.toString().trim { it <= ' ' }
val strUrl = etStreamListenerConfigurationUrl.text.toString().trim { it <= ' ' }
stream_config_json = null
showButtonState()
val task = object : AsyncTask<Void, Void, String?>() {
override fun doInBackground(vararg params : Void) : String? {
try {
while(true) {
if(! Pref.bpSendAccessTokenToAppServer(Pref.pref(this@ActCustomStreamListener))) {
addLog("we won't use push notification until 'SendAccessTokenToAppServer' is not set.")
break
}
if(strSecret.isEmpty()) {
addLog("Secret is empty. Custom Listener is not used.")
break
} else if(strUrl.isEmpty()) {
addLog("Configuration URL is empty. Custom Listener is not used.")
break
}
addLog("try to loading Configuration data from URL…")
var builder : Request.Builder = Request.Builder()
.url(strUrl)
var call = App1.ok_http_client.newCall(builder.build())
val response = call.execute()
val bodyString : String? = try {
response.body()?.string()
} catch(ex : Throwable) {
log.trace(ex)
null
}
if(! response.isSuccessful || bodyString?.isEmpty() != false) {
addLog(
TootApiClient.formatResponse(
response,
"Can't get configuration from URL.",
bodyString
)
)
break
}
val jv : JsonValue = try {
JsonValue.readHjson(bodyString)
} catch(ex : Throwable) {
log.trace(ex)
addLog(ex.withCaption("Can't parse configuration data."))
break
}
if(! jv.isObject) {
addLog("configuration data is not JSON Object.")
break
}
val root = jv.asObject()
var has_wildcard = false
var has_error = false
for(member in root) {
val strInstance = member.name
if("*" == strInstance) {
has_wildcard = true
} else if(reUpperCase.matcher(strInstance).find()) {
addLog("$strInstance : instance URL must be lower case.")
has_error = true
continue
} else if(strInstance[strInstance.length - 1] == '/') {
addLog("$strInstance : instance URL must not be trailed with '/'.")
has_error = true
continue
} else if(! reInstanceURL.matcher(strInstance).find()) {
addLog("$strInstance : instance URL is not like https://.....")
has_error = true
continue
}
val entry_value = member.value
if(! entry_value.isObject) {
addLog("$strInstance : value for this instance is not JSON Object.")
has_error = true
continue
}
val entry = entry_value.asObject()
val keys = arrayOf(
"urlStreamingListenerRegister",
"urlStreamingListenerUnregister",
"appId"
)
for(key in keys) {
val v = entry.get(key)
if(! v.isString) {
addLog("$strInstance.$key : missing parameter, or data type is not string.")
has_error = true
continue
}
val sv = v.asString()
if(sv.isEmpty()) {
addLog("$strInstance.$key : empty parameter.")
has_error = true
} else if(sv.contains(" ")) {
addLog("$strInstance.$key : contains whitespace.")
has_error = true
}
if("appId" != key) {
if(! reUrl.matcher(sv).find()) {
addLog("$strInstance.$key : not like Url.")
has_error = true
} else if(sv.startsWith("https://")) {
try {
addLog("check access to $sv")
builder = Request.Builder().url(sv)
call = App1.ok_http_client.newCall(builder.build())
call.execute()
} catch(ex : Throwable) {
log.trace(ex)
addLog(ex.withCaption("$strInstance.$key : connect failed."))
has_error = true
}
}
}
}
}
if(! has_wildcard) {
addLog("Warning: This configuration has no wildcard entry.")
if(! has_error) {
for(sa in SavedAccount.loadAccountList(this@ActCustomStreamListener)) {
if(sa.isPseudo) continue
val instanceUrl = ("https://" + sa.host).toLowerCase()
val v = root.get(instanceUrl)
if(v == null || ! v.isObject) {
addLog("Warning: $instanceUrl : is found in account, but not found in configuration data.")
}
}
}
}
if(has_error) {
addLog("This configuration has error. ")
break
}
return bodyString
}
} catch(ex : Throwable) {
log.trace(ex)
addLog(ex.withCaption("Can't read configuration from URL."))
}
return null
}
override fun onCancelled(s : String?) {
onPostExecute(s)
}
override fun onPostExecute(s : String?) {
last_task = null
if(s != null) {
stream_config_json = s
addLog("seems configuration is ok.")
} else {
addLog("error detected.")
}
showButtonState()
}
}
last_task = task
task.executeOnExecutor(App1.task_executor)
}
}

View File

@ -20,38 +20,27 @@ import android.os.PowerManager
import android.os.SystemClock
import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat
import com.google.firebase.iid.FirebaseInstanceId
import jp.juggler.subwaytooter.api.*
import org.hjson.JsonObject
import org.hjson.JsonValue
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.lang.ref.WeakReference
import java.util.ArrayList
import java.util.Collections
import java.util.Comparator
import java.util.HashSet
import java.util.LinkedList
import java.util.TreeSet
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.EntityIdLong
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.util.*
import jp.juggler.util.*
import okhttp3.Call
import okhttp3.Request
import okhttp3.RequestBody
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
class PollingWorker private constructor(contextArg : Context) {
@ -102,7 +91,6 @@ class PollingWorker private constructor(contextArg : Context) {
const val TASK_NOTIFICATION_DELETE = 10
const val TASK_NOTIFICATION_CLICK = 11
private const val TASK_UPDATE_NOTIFICATION = 12
private const val TASK_UPDATE_LISTENER = 13
@SuppressLint("StaticFieldLeak")
private var sInstance : PollingWorker? = null
@ -266,10 +254,6 @@ class PollingWorker private constructor(contextArg : Context) {
}
fun queueUpdateListener(context : Context) {
addTask(context, true, TASK_UPDATE_LISTENER, null)
}
fun queueUpdateNotification(context : Context) {
addTask(context, true, TASK_UPDATE_NOTIFICATION, null)
}
@ -420,7 +404,7 @@ class PollingWorker private constructor(contextArg : Context) {
private val appState : AppState
internal val handler : Handler
internal val pref : SharedPreferences
internal val connectivityManager : ConnectivityManager
private val connectivityManager : ConnectivityManager
internal val notification_manager : NotificationManager
internal val scheduler : JobScheduler
private val power_manager : PowerManager?
@ -819,10 +803,6 @@ class PollingWorker private constructor(contextArg : Context) {
internal inner class TaskRunner {
var mCustomStreamListenerSecret : String? = null
var mCustomStreamListenerSettingString : String? = null
private var mCustomStreamListenerSetting : JsonObject? = null
lateinit var job : JobItem
private var taskId : Int = 0
@ -938,8 +918,6 @@ class PollingWorker private constructor(contextArg : Context) {
}
loadCustomStreamListenerSetting()
job_status.set("make install id")
// インストールIDを生成する
@ -1051,22 +1029,6 @@ class PollingWorker private constructor(contextArg : Context) {
notification_manager.notify(NOTIFICATION_ID_ERROR, builder.build())
}
private fun loadCustomStreamListenerSetting() {
mCustomStreamListenerSetting = null
mCustomStreamListenerSecret = null
val jsonString = Pref.spStreamListenerConfigData(pref)
mCustomStreamListenerSettingString = jsonString
if(jsonString.isNotEmpty()) {
try {
mCustomStreamListenerSetting = JsonValue.readHjson(jsonString).asObject()
mCustomStreamListenerSecret = Pref.spStreamListenerSecret(pref)
} catch(ex : Throwable) {
log.trace(ex)
}
}
}
internal inner class AccountThread(
val account : SavedAccount
) : Thread(), CurrentCallCallback {
@ -1123,18 +1085,8 @@ class PollingWorker private constructor(contextArg : Context) {
}
if(job.isJobCancelled) return
if(wps.flags == 0) {
if(! account.isMisskey) unregisterDeviceToken()
return
}
if(wps.subscribed) {
if(! account.isMisskey) unregisterDeviceToken()
} else {
if(! account.isMisskey) registerDeviceToken()
}
if(job.isJobCancelled) return
if(wps.flags == 0) return
checkAccount()
@ -1149,155 +1101,6 @@ class PollingWorker private constructor(contextArg : Context) {
}
}
private fun unregisterDeviceToken() {
try {
if(SavedAccount.REGISTER_KEY_UNREGISTERED == account.register_key) {
log.d("unregisterDeviceToken: already unregistered.")
return
}
// ネットワーク的な事情でインストールIDを取得できなかったのなら、何もしない
val install_id = job.install_id
if(install_id?.isEmpty() != false) {
log.d("unregisterDeviceToken: missing install_id")
return
}
val tag = account.notification_tag
if(tag?.isEmpty() != false) {
log.d("unregisterDeviceToken: missing notification_tag")
return
}
val post_data = ("instance_url=" + ("https://" + account.host).encodePercent()
+ "&app_id=" + context.packageName.encodePercent()
+ "&tag=" + tag)
val request = post_data.toRequestBody().toPost()
.url("$APP_SERVER/unregister")
.build()
val call = App1.ok_http_client.newCall(request)
current_call = call
val response = call.execute()
log.d("unregisterDeviceToken: %s", response)
if(response.isSuccessful) {
account.register_key = SavedAccount.REGISTER_KEY_UNREGISTERED
account.register_time = 0L
account.saveRegisterKey()
}
} catch(ex : Throwable) {
log.trace(ex)
}
}
private fun registerDeviceToken() {
try {
// 設定によってはデバイストークンやアクセストークンを送信しない
if(! Pref.bpSendAccessTokenToAppServer(Pref.pref(context))) {
log.d("registerDeviceToken: SendAccessTokenToAppServer is not set.")
return
}
// ネットワーク的な事情でインストールIDを取得できなかったのなら、何もしない
val install_id = job.install_id
if(install_id?.isEmpty() != false) {
log.d("registerDeviceToken: missing install id")
return
}
val prefDevice = PrefDevice.prefDevice(context)
val device_token = prefDevice.getString(PrefDevice.KEY_DEVICE_TOKEN, null)
if(device_token?.isEmpty() != false) {
log.d("registerDeviceToken: missing device_token")
return
}
val access_token = account.getAccessToken()
if(access_token?.isEmpty() != false) {
log.d("registerDeviceToken: missing access_token")
return
}
var tag : String? = account.notification_tag
if(SavedAccount.REGISTER_KEY_UNREGISTERED == account.register_key) {
tag = null
}
if(tag?.isEmpty() != false) {
account.notification_tag =
(job.install_id + account.db_id + account.acct).digestSHA256Hex()
tag = account.notification_tag
account.saveNotificationTag()
}
val reg_key = (tag
+ access_token
+ device_token
+ (if(mCustomStreamListenerSecret == null) "" else mCustomStreamListenerSecret)
+ if(mCustomStreamListenerSettingString == null) "" else mCustomStreamListenerSettingString
).digestSHA256Hex()
val now = System.currentTimeMillis()
if(reg_key == account.register_key && now - account.register_time < 3600000 * 3) {
// タグやトークンが同一なら、前回登録に成功してから一定時間は再登録しない
log.d("registerDeviceToken: already registered.")
return
}
val post_data = StringBuilder()
.append("instance_url=").append(("https://" + account.host).encodePercent())
.append("&app_id=").append(context.packageName.encodePercent())
.append("&tag=").append(tag)
.append("&access_token=").append(access_token)
.append("&device_token=").append(device_token)
val jsonString = mCustomStreamListenerSettingString
val appSecret = mCustomStreamListenerSecret
if(jsonString != null && appSecret != null) {
post_data.append("&user_config=").append(jsonString.encodePercent())
post_data.append("&app_secret=").append(appSecret.encodePercent())
}
val request = post_data.toString().toRequestBody().toPost()
.url("$APP_SERVER/register")
.build()
val call = App1.ok_http_client.newCall(request)
current_call = call
val response = call.execute()
var body : String? = null
try {
body = response.body()?.string()
} catch(ignored : Throwable) {
}
log.d("registerDeviceToken: %s (%s)", response, body ?: "")
val code = response.code()
if(response.isSuccessful || code >= 400 && code < 500) {
// 登録できた時も4xxエラーだった時もDBに記録する
account.register_key = reg_key
account.register_time = now
account.saveRegisterKey()
}
} catch(ex : Throwable) {
log.trace(ex)
}
}
private fun checkAccount() {
this.nr = NotificationTracking.load(account.db_id)
this.parser = TootParser(context, account)

View File

@ -328,13 +328,6 @@ object Pref {
R.id.swVerticalArrangeThumbnails
)
val bpSendAccessTokenToAppServer = BooleanPref(
"SendAccessTokenToAppServer",
false,
R.id.swSendAccessTokenToAppServer
)
val bpDontShowPreviewCard = BooleanPref(
"DontShowPreviewCard",
false,
@ -418,9 +411,6 @@ object Pref {
val spMovieSizeMax = StringPref("max_movie_size", "40")
val spTimelineFont = StringPref("timeline_font", "", skipImport = true)
val spTimelineFontBold = StringPref("timeline_font_bold", "", skipImport = true)
val spStreamListenerSecret = StringPref("stream_listener_secret", "")
val spStreamListenerConfigUrl = StringPref("stream_listener_config_url", "")
val spStreamListenerConfigData = StringPref("stream_listener_config_data", "")
val spMspUserToken = StringPref("mastodon_search_portal_user_token", "")
val spEmojiPickerRecent = StringPref("emoji_picker_recent", "")
val spRoundRatio = StringPref("round_ratio", "33")

View File

@ -21,7 +21,7 @@ class DuplicateMap {
val uri = o.uri
val url = o.url
when {
uri?.isNotEmpty() == true -> {
uri.isNotEmpty() -> {
if(set_uri.contains(uri)) return true
set_uri.add(uri)
}

View File

@ -1496,7 +1496,7 @@ fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : TootA
return result
}
val uri = obj.uri
if(uri?.isNotEmpty() == true) {
if(uri.isNotEmpty() ) {
url = uri
}
}

View File

@ -87,69 +87,6 @@
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/send_access_token_to_app_server"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swSendAccessTokenToAppServer"
style="@style/setting_horizontal_stretch"
android:gravity="center"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<TextView
style="@style/setting_horizontal_stretch"
android:text="@string/send_access_token_to_app_server_desc"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/custom_stream_listener"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnCustomStreamListenerEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnCustomStreamListenerReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<TextView
style="@style/setting_horizontal_stretch"
android:text="@string/custom_stream_listener_desc"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
</LinearLayout>

View File

@ -1,112 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/llContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:labelFor="@+id/etStreamListenerConfigurationUrl"
android:text="@string/configuration_url"
/>
<LinearLayout style="@style/setting_row_form">
<EditText
android:id="@+id/etStreamListenerConfigurationUrl"
style="@style/setting_horizontal_stretch"
android:inputType="textUri"
android:maxLines="1"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:labelFor="@+id/etStreamListenerSecret"
android:text="@string/secret"
/>
<LinearLayout style="@style/setting_row_form">
<EditText
android:id="@+id/etStreamListenerSecret"
style="@style/setting_horizontal_stretch"
android:inputType="textPassword"
android:maxLines="1"
/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/custom_stream_listener_desc"
android:textSize="12sp"
/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fadeScrollbars="false"
android:fillViewport="true"
>
<TextView
android:id="@+id/tvLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
/>
</ScrollView>
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureWithLargestChild="true"
>
<Button
android:id="@+id/btnDiscard"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/discard"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnTest"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/test"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnSave"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/save"
android:textAllCaps="false"
/>
</LinearLayout>
</LinearLayout>

View File

@ -271,9 +271,6 @@
<string name="app_data_export">Allforio data\'r ap</string>
<string name="app_data_import">Mewnforio data\'r ap</string>
<string name="draft_deleted">Drafft wedi\'i ddileu.</string>
<string name="secret">Cyfrinachol</string>
<string name="test">Profi</string>
<string name="custom_stream_listener_desc">Mae\'r gosodiad hwn ar gyfer defnyddwyr profiadol. Os nad ydych chi\'n siŵr, peidiwch a golygu. </string>
<string name="language_code">en</string>
<string name="performance">Ymddangosiad</string>
<string name="behavior">Ymddygiad</string>
@ -403,7 +400,6 @@
<string name="locked_account">Cyfrif wedi\'i gloi</string>
<string name="languages">ieithoedd</string>
<string name="changed">newidwyd.</string>
<string name="send_access_token_to_app_server">Anfon tocyn mynediad i weinydd yr ap</string>
<string name="follow_accept">derbyn</string>
<string name="follow_deny">gwrthod</string>
<string name="follow_accept_confirm">Derbyn y cais dilyn o %1$s\?</string>

View File

@ -357,13 +357,6 @@
<string name="draft_deleted">Brouillon effacé.</string>
<string name="draft_picker_desc">Appui long pour effacer.</string>
<string name="dont_crop_media_thumbnail">Ne pas recadrer les aperçus de pièces jointes\n(redémarrage nécessaire)</string>
<string name="custom_stream_listener">Écouteur de notification personnalisé</string>
<string name="configuration_url">Configuration URL</string>
<string name="secret">Secret</string>
<string name="test">Tester</string>
<string name="custom_stream_listener_desc">Réglages réservés aux utilisateurs avertis. Si vous ne savez pas de quoi il s\'agit, laissez tel quel.</string>
<string name="custom_stream_listener_was_reset">Custom notification listener réinitialisé.</string>
<string name="input_url_and_secret_then_test">Entrez les informations d\'URL et de Secret, appuyez ensuite sur le bouton [Tester].</string>
<string name="tab_indicator_color">Couleur de la ligne d\'indication des colonnes affichées</string>
<string name="plugin">Plugin</string>
<string name="select_plugin">Sélection du plugin</string>
@ -652,10 +645,6 @@
<string name="changed">a changé.</string>
<string name="thumbnails_arrange_vertically">Disposition verticale des vignettes (redémarrage de l\'application requis)</string>
<string name="instance_local">Instance locale</string>
<string name="send_access_token_to_app_server">Envoyer le jeton d\'accès au serveur de lapplication</string>
<string name="send_access_token_to_app_server_desc">Normally you can use \"pull\" notification (with delay) without app server, because it works on your device.
\nBut if you want to use \"custom notification listener\", or if you are the member of very limited instances that is supported by ST\'s app server,
\nyou can use \"push\" notifications, but you have to allow sending access tokens to the app server. (this is mastodon\'s defect)</string>
<string name="pull_notification_check_interval">Intervalle de vérification des notifications (unité : minutes, par défaut : 15, min:15)</string>
<string name="follow_accept">accepter</string>
<string name="follow_deny">refuser</string>

View File

@ -142,7 +142,6 @@
<string name="column_list">カラム一覧</string>
<string name="column_list_desc">スワイプで削除します。並べ替えと削除は戻るまたは選択した時に反映されます</string>
<string name="comment_empty">通報する理由を書いてください</string>
<string name="configuration_url">設定情報URL</string>
<string name="confirm">確認</string>
<string name="confirm_account_remove">アカウントをこのアプリから除去しますか?関連するカラムはすべて除去されます</string>
<string name="confirm_before_boost">ブースト前に確認</string>
@ -181,9 +180,6 @@
<string name="copy">コピー</string>
<string name="copy_complete">クリップボードにコピーしました</string>
<string name="copy_url">URLをクリップボードにコピー</string>
<string name="custom_stream_listener">カスタム通知リスナ</string>
<string name="custom_stream_listener_desc">この設定は上級者向けです。よく分からない場合は編集しないでください。失敗するとリアルタイム通知が動作しなくなります。</string>
<string name="custom_stream_listener_was_reset">カスタム通知リスナを使わないようにしました</string>
<string name="default_status_visibility">投稿の公開範囲の既定値</string>
<string name="delete">削除</string>
<string name="delete_base_status_before_toot">元トゥートを削除して投稿します。お気に入りとブーストは失われます。返信は切断されます。マストドン2.4.1未満の場合は添付メディアは維持されません。よろしいですか?</string>
@ -355,7 +351,6 @@
<string name="image_capture">カメラで撮影</string>
<string name="in_reply_to_id_conversion_failed">アカウント切り替えできません。in_reply_toのID変換に失敗しました。</string>
<string name="input_access_token">アクセストークンを指定する(上級者向け)</string>
<string name="input_url_and_secret_then_test">URLとシークレットを入力してテストボタンを押してください</string>
<string name="instance">インスタンス</string>
<string name="instance_does_not_support_push_api">タンスのバージョン %1$s は古くてプッシュ購読を利用できません</string>
<string name="instance_hint">例: mastodon.social</string>
@ -608,13 +603,10 @@
<string name="search_is_not_available_on_pseudo_account">疑似アカウントでは検索APIを利用できなくなりました</string>
<string name="search_of">検索:%1$s</string>
<string name="search_web">Web検索</string>
<string name="secret">シークレット</string>
<string name="select_and_copy">選択してコピー…</string>
<string name="select_draft">どの下書きから復元しますか?</string>
<string name="select_plugin">プラグインの選択</string>
<string name="send">共有</string>
<string name="send_access_token_to_app_server">アクセストークンをアプリサーバに送信する</string>
<string name="send_access_token_to_app_server_desc">通常は\"pull\"通知(すこし遅れる)を利用できます。それは端末上で動作するのでアプリサーバは必要ありません。しかしカスタム通知リスナを使う場合やSTがサポートする限られたインスタンスのユーザである場合は\"push\"通知を利用することができます。ただしアクセストークンをアプリサーバに送信する必要があります(これはマストドンの欠陥です)。</string>
<string name="send_header_account_acct">Account-Acct</string>
<string name="send_header_account_created_at">Account-Created-At</string>
<string name="send_header_account_followers_count">Account-Followers-Count</string>
@ -672,7 +664,6 @@
<string name="tablet_mode">タブレットモード</string>
<string name="tap_to_show">タップで表示</string>
<string name="target_user">対象ユーザ</string>
<string name="test">テスト</string>
<string name="text_color">文字色</string>
<string name="text_to_speech_initialize_failed">TextToSpeechの初期化に失敗。status=%1$s</string>
<string name="text_to_speech_initializing">TextToSpeechの初期化中…</string>

View File

@ -293,10 +293,6 @@
<string name="app_data_import">Importer programdata</string>
<string name="draft_picker_desc">Trykk lenge for å slette.</string>
<string name="draft_deleted">Kladd slettet.</string>
<string name="configuration_url">Oppsettsnettadresse</string>
<string name="secret">Hemmeldighet</string>
<string name="test">Test</string>
<string name="custom_stream_listener_desc">Denne innstillinger er myntet på avanserte brukere. Hvis du er usikker, la være å endre.</string>
<string name="plugin">Programtillegg</string>
<string name="select_plugin">Velg programtillegg</string>
<string name="plugin_not_installed">Programtillegg ikke installert.</string>
@ -500,7 +496,6 @@
<string name="contact">kontaktkonto</string>
<string name="languages">språk</string>
<string name="changed">endret.</string>
<string name="send_access_token_to_app_server">Send tilgangssymbol til programtjener</string>
<string name="follow_accept">godta</string>
<string name="follow_deny">forkast</string>
<string name="follow_accept_confirm">Godta følgingsforespørselen fra %1$s?</string>
@ -648,9 +643,6 @@
<string name="app_data_import_confirm">eksisterende data (før import) vil bli tømt. Er du sikker\?</string>
<string name="user_id_conversion_failed">Kunne ikke konvertere bruker-ID</string>
<string name="dont_crop_media_thumbnail">Ikke beskjær mediaminiatyrbilde (programstart kreves)</string>
<string name="custom_stream_listener">Egendefinert merknadslytter</string>
<string name="custom_stream_listener_was_reset">Egendefinert merknadslytter tilbakestilt.</string>
<string name="input_url_and_secret_then_test">Skriv inn nettadresse og hemmelighet, trykk så på \"Test\".</string>
<string name="notification_on_off_desc">Sjekk også kontoinnstillingen for å skru på/av merknad.</string>
<string name="in_reply_to_id_conversion_failed">Kunne ikke konvertere \"in_reply_to\"-ID</string>
<string name="prior_chrome_custom_tabs">Tidligere Google Chrome (hvis egendefinerte faner er påslått)</string>
@ -711,9 +703,6 @@
<string name="system_notification_not_related">Ikke valgt når systemmerknad trykkes</string>
<string name="append_attachment_url_to_content">Legg til vedleggsnettadresse i bindeleddstekst</string>
<string name="instance_local">Lokal instans</string>
<string name="send_access_token_to_app_server_desc">Normalt kan du bruke \"pull\"-merknader (med forsinkelse) uten programtjener, fordi det virker på din enhet.
\nHvis du ønsker å bruke \"egendefinert merknadslytter\", eller hvis du er medlem av veldig begrensede instanser som støttes av STs programtjener,
\nkan du bruke \"push\"-merknader, men du må tillate innsending av tilgangssymboler til programtjeneren. (Dette er en begrensning i Mastodon).</string>
<string name="pull_notification_check_interval">Sjekkintervall for pull-merknad (enhet: minutter, forvalg:15, min:15)</string>
<string name="token_exported">Som følge av eksport av programdata eller gjenoppretting fra sikkerhetskopi, brukes dette tilgangssymbolet også av andre enheter. Fordi tilgangssymboler som ikke brukes av andre enheter kreves for å bruke push-merknader, anbefales oppdatering av tilgangssymbolet.</string>
<string name="missing_push_api">Instansen har inget API for push-merknader.</string>

View File

@ -358,13 +358,6 @@
<string name="draft_picker_desc">Long tap to delete.</string>
<string name="draft_deleted">Draft deleted.</string>
<string name="dont_crop_media_thumbnail">Don\'t crop media thumbnail (app restart required)</string>
<string name="custom_stream_listener">Custom notification listener</string>
<string name="configuration_url">Configuration URL</string>
<string name="secret">Secret</string>
<string name="test">Test</string>
<string name="custom_stream_listener_desc">This setting is for advanced users. If you are not sure, please don\'t edit.</string>
<string name="custom_stream_listener_was_reset">Custom notification listener was reset.</string>
<string name="input_url_and_secret_then_test">Input URL and Secret, then tap Test button.</string>
<string name="tab_indicator_color">Tab\'s indicator color</string>
<string name="plugin">Plugin</string>
<string name="select_plugin">Select plugin</string>
@ -645,10 +638,6 @@
<string name="changed">changed.</string>
<string name="thumbnails_arrange_vertically">Vertical arrange thumbnails (app restart required)</string>
<string name="instance_local">Instance local</string>
<string name="send_access_token_to_app_server">Send access token to app server</string>
<string name="send_access_token_to_app_server_desc">Normally you can use \"pull\" notification (with delay) without app server, because it works on your device.
\nBut if you want to use \"custom notification listener\", or if you are the member of very limited instances that is supported by ST\'s app server,
\nyou can use \"push\" notifications, but you have to allow sending access tokens to the app server. (this is mastodon\'s defect)</string>
<string name="pull_notification_check_interval">Pull notification check interval (unit: minutes, default:15, min:15)</string>
<string name="follow_accept">accept</string>
<string name="follow_deny">deny</string>