521 lines
23 KiB
Kotlin
521 lines
23 KiB
Kotlin
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package org.mariotaku.twidere.service
|
|
|
|
import android.accounts.AccountManager
|
|
import android.annotation.SuppressLint
|
|
import android.app.Notification
|
|
import android.app.Service
|
|
import android.content.ContentValues
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.provider.BaseColumns
|
|
import android.support.annotation.UiThread
|
|
import android.support.annotation.WorkerThread
|
|
import android.support.v4.app.NotificationCompat
|
|
import android.support.v4.app.NotificationCompat.Builder
|
|
import android.text.TextUtils
|
|
import android.util.Log
|
|
import android.widget.Toast
|
|
import edu.tsinghua.hotmobi.HotMobiLogger
|
|
import edu.tsinghua.hotmobi.model.TimelineType
|
|
import edu.tsinghua.hotmobi.model.TweetEvent
|
|
import nl.komponents.kovenant.task
|
|
import nl.komponents.kovenant.ui.successUi
|
|
import org.mariotaku.abstask.library.AbstractTask
|
|
import org.mariotaku.abstask.library.ManualTaskStarter
|
|
import org.mariotaku.ktextension.configure
|
|
import org.mariotaku.ktextension.toLong
|
|
import org.mariotaku.ktextension.toTypedArray
|
|
import org.mariotaku.ktextension.useCursor
|
|
import org.mariotaku.microblog.library.MicroBlog
|
|
import org.mariotaku.microblog.library.MicroBlogException
|
|
import org.mariotaku.microblog.library.twitter.TwitterUpload
|
|
import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse
|
|
import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse.ProcessingInfo
|
|
import org.mariotaku.restfu.http.ContentType
|
|
import org.mariotaku.restfu.http.mime.Body
|
|
import org.mariotaku.restfu.http.mime.SimpleBody
|
|
import org.mariotaku.sqliteqb.library.Expression
|
|
import org.mariotaku.twidere.R
|
|
import org.mariotaku.twidere.TwidereConstants.*
|
|
import org.mariotaku.twidere.annotation.AccountType
|
|
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
|
|
import org.mariotaku.twidere.model.*
|
|
import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtras
|
|
import org.mariotaku.twidere.model.draft.StatusObjectExtras
|
|
import org.mariotaku.twidere.model.util.AccountUtils
|
|
import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils
|
|
import org.mariotaku.twidere.model.util.ParcelableStatusUpdateUtils
|
|
import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages
|
|
import org.mariotaku.twidere.provider.TwidereDataStore.Drafts
|
|
import org.mariotaku.twidere.task.CreateFavoriteTask
|
|
import org.mariotaku.twidere.task.RetweetStatusTask
|
|
import org.mariotaku.twidere.task.twitter.UpdateStatusTask
|
|
import org.mariotaku.twidere.util.ContentValuesCreator
|
|
import org.mariotaku.twidere.util.NotificationManagerWrapper
|
|
import org.mariotaku.twidere.util.Utils
|
|
import org.mariotaku.twidere.util.deleteDrafts
|
|
import org.mariotaku.twidere.util.io.ContentLengthInputStream.ReadListener
|
|
import java.io.IOException
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
/**
|
|
* Intent service for lengthy operations like update status/send DM.
|
|
*/
|
|
class LengthyOperationsService : BaseIntentService("lengthy_operations") {
|
|
|
|
private val handler: Handler by lazy { Handler(Looper.getMainLooper()) }
|
|
|
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
super.onStartCommand(intent, flags, startId)
|
|
return Service.START_STICKY
|
|
}
|
|
|
|
override fun onHandleIntent(intent: Intent?) {
|
|
if (intent == null) return
|
|
val action = intent.action ?: return
|
|
when (action) {
|
|
INTENT_ACTION_UPDATE_STATUS -> {
|
|
handleUpdateStatusIntent(intent)
|
|
}
|
|
INTENT_ACTION_SEND_DIRECT_MESSAGE -> {
|
|
handleSendDirectMessageIntent(intent)
|
|
}
|
|
INTENT_ACTION_DISCARD_DRAFT -> {
|
|
handleDiscardDraftIntent(intent)
|
|
}
|
|
INTENT_ACTION_SEND_DRAFT -> {
|
|
handleSendDraftIntent(intent)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun showErrorMessage(actionRes: Int, e: Exception?, longMessage: Boolean) {
|
|
handler.post { Utils.showErrorMessage(this@LengthyOperationsService, actionRes, e, longMessage) }
|
|
}
|
|
|
|
private fun showOkMessage(message: Int, longMessage: Boolean) {
|
|
handler.post { Toast.makeText(this@LengthyOperationsService, message, if (longMessage) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show() }
|
|
}
|
|
|
|
private fun handleSendDraftIntent(intent: Intent) {
|
|
val uri = intent.data ?: return
|
|
notificationManager.cancel(uri.toString(), NOTIFICATION_ID_DRAFTS)
|
|
val draftId = uri.lastPathSegment.toLong(-1)
|
|
if (draftId == -1L) return
|
|
val where = Expression.equals(Drafts._ID, draftId)
|
|
@SuppressLint("Recycle")
|
|
val draft: Draft = contentResolver.query(Drafts.CONTENT_URI, Drafts.COLUMNS, where.sql, null, null)?.useCursor {
|
|
val i = DraftCursorIndices(it)
|
|
if (!it.moveToFirst()) return@useCursor null
|
|
return@useCursor i.newObject(it)
|
|
} ?: return
|
|
|
|
contentResolver.delete(Drafts.CONTENT_URI, where.sql, null)
|
|
if (TextUtils.isEmpty(draft.action_type)) {
|
|
draft.action_type = Draft.Action.UPDATE_STATUS
|
|
}
|
|
when (draft.action_type) {
|
|
Draft.Action.UPDATE_STATUS_COMPAT_1, Draft.Action.UPDATE_STATUS_COMPAT_2,
|
|
Draft.Action.UPDATE_STATUS, Draft.Action.REPLY, Draft.Action.QUOTE -> {
|
|
updateStatuses(draft.action_type, ParcelableStatusUpdateUtils.fromDraftItem(this, draft))
|
|
}
|
|
Draft.Action.SEND_DIRECT_MESSAGE_COMPAT, Draft.Action.SEND_DIRECT_MESSAGE -> {
|
|
val recipientId = (draft.action_extras as? SendDirectMessageActionExtras)?.recipientId ?: return
|
|
val accountKey = draft.account_keys?.firstOrNull() ?: return
|
|
val imageUri = draft.media.firstOrNull()?.uri
|
|
sendMessage(accountKey, recipientId, draft.text, imageUri)
|
|
}
|
|
Draft.Action.FAVORITE -> {
|
|
performStatusAction(draft) { accountKey, status ->
|
|
CreateFavoriteTask(this, accountKey, status)
|
|
}
|
|
}
|
|
Draft.Action.RETWEET -> {
|
|
performStatusAction(draft) { accountKey, status ->
|
|
RetweetStatusTask(this, accountKey, status)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressLint("Recycle")
|
|
private fun handleDiscardDraftIntent(intent: Intent) {
|
|
val data = intent.data ?: return
|
|
task {
|
|
if (deleteDrafts(this, longArrayOf(data.lastPathSegment.toLong(-1))) < 1) {
|
|
throw IOException()
|
|
}
|
|
return@task data
|
|
}.successUi { uri ->
|
|
notificationManager.cancel(data.toString(), NOTIFICATION_ID_DRAFTS)
|
|
}
|
|
}
|
|
|
|
private fun handleSendDirectMessageIntent(intent: Intent) {
|
|
val accountId = intent.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
|
|
val recipientId = intent.getStringExtra(EXTRA_RECIPIENT_ID)
|
|
val text = intent.getStringExtra(EXTRA_TEXT)
|
|
val imageUri = intent.getStringExtra(EXTRA_IMAGE_URI)
|
|
if (accountId == null || recipientId == null || text == null) return
|
|
sendMessage(accountId, recipientId, text, imageUri)
|
|
}
|
|
|
|
private fun sendMessage(accountId: UserKey, recipientId: String,
|
|
text: String, imageUri: String?) {
|
|
val title = getString(R.string.sending_direct_message)
|
|
val builder = Builder(this)
|
|
builder.setSmallIcon(R.drawable.ic_stat_send)
|
|
builder.setProgress(100, 0, true)
|
|
builder.setTicker(title)
|
|
builder.setContentTitle(title)
|
|
builder.setContentText(text)
|
|
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
|
builder.setOngoing(true)
|
|
val notification = builder.build()
|
|
startForeground(NOTIFICATION_ID_SEND_DIRECT_MESSAGE, notification)
|
|
val result = sendDirectMessage(builder, accountId,
|
|
recipientId, text, imageUri)
|
|
|
|
val resolver = contentResolver
|
|
if (result.hasData()) {
|
|
val message = result.data!!
|
|
val values = ContentValuesCreator.createDirectMessage(message)
|
|
val deleteWhere = Expression.and(Expression.equalsArgs(DirectMessages.ACCOUNT_KEY),
|
|
Expression.equalsArgs(DirectMessages.MESSAGE_ID)).sql
|
|
val deleteWhereArgs = arrayOf(message.account_key.toString(), message.id)
|
|
resolver.delete(DirectMessages.Outbox.CONTENT_URI, deleteWhere, deleteWhereArgs)
|
|
resolver.insert(DirectMessages.Outbox.CONTENT_URI, values)
|
|
showOkMessage(R.string.message_direct_message_sent, false)
|
|
} else {
|
|
val values = ContentValuesCreator.createMessageDraft(accountId, recipientId, text, imageUri)
|
|
resolver.insert(Drafts.CONTENT_URI, values)
|
|
showErrorMessage(R.string.action_sending_direct_message, result.exception, true)
|
|
}
|
|
stopForeground(false)
|
|
notificationManager.cancel(NOTIFICATION_ID_SEND_DIRECT_MESSAGE)
|
|
}
|
|
|
|
private fun handleUpdateStatusIntent(intent: Intent) {
|
|
val status = intent.getParcelableExtra<ParcelableStatusUpdate>(EXTRA_STATUS)
|
|
val statusParcelables = intent.getParcelableArrayExtra(EXTRA_STATUSES)
|
|
val statuses: Array<ParcelableStatusUpdate>
|
|
if (statusParcelables != null) {
|
|
statuses = statusParcelables.toTypedArray(ParcelableStatusUpdate.CREATOR)
|
|
} else if (status != null) {
|
|
statuses = arrayOf(status)
|
|
} else
|
|
return
|
|
@Draft.Action
|
|
val actionType = intent.getStringExtra(EXTRA_ACTION)
|
|
updateStatuses(actionType, *statuses)
|
|
}
|
|
|
|
private fun updateStatuses(@Draft.Action actionType: String, vararg statuses: ParcelableStatusUpdate) {
|
|
val context = this
|
|
val builder = Builder(context)
|
|
startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context,
|
|
builder, 0, null))
|
|
for (item in statuses) {
|
|
val task = UpdateStatusTask(context, object : UpdateStatusTask.StateCallback {
|
|
|
|
@WorkerThread
|
|
override fun onStartUploadingMedia() {
|
|
startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context,
|
|
builder, 0, item))
|
|
}
|
|
|
|
@WorkerThread
|
|
override fun onUploadingProgressChanged(index: Int, current: Long, total: Long) {
|
|
val progress = (current * 100 / total).toInt()
|
|
startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context,
|
|
builder, progress, item))
|
|
}
|
|
|
|
@WorkerThread
|
|
override fun onShorteningStatus() {
|
|
startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context,
|
|
builder, 0, item))
|
|
}
|
|
|
|
@WorkerThread
|
|
override fun onUpdatingStatus() {
|
|
startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context,
|
|
builder, 0, item))
|
|
}
|
|
|
|
@UiThread
|
|
override fun afterExecute(result: UpdateStatusTask.UpdateStatusResult) {
|
|
var failed = false
|
|
val exception = result.exception
|
|
val exceptions = result.exceptions
|
|
if (exception != null) {
|
|
val cause = exception.cause
|
|
if (cause is MicroBlogException) {
|
|
Toast.makeText(context, cause.errors?.firstOrNull()?.message ?: cause.message,
|
|
Toast.LENGTH_SHORT).show()
|
|
} else {
|
|
Toast.makeText(context, exception.message, Toast.LENGTH_SHORT).show()
|
|
}
|
|
failed = true
|
|
Log.w(LOGTAG, exception)
|
|
} else for (e in exceptions) {
|
|
if (e != null) {
|
|
// Show error
|
|
var errorMessage = Utils.getErrorMessage(context, e)
|
|
if (TextUtils.isEmpty(errorMessage)) {
|
|
errorMessage = context.getString(R.string.status_not_updated)
|
|
}
|
|
Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
|
|
failed = true
|
|
break
|
|
}
|
|
}
|
|
if (!failed) {
|
|
Toast.makeText(context, R.string.message_toast_status_updated, Toast.LENGTH_SHORT).show()
|
|
}
|
|
}
|
|
|
|
override fun beforeExecute() {
|
|
|
|
}
|
|
})
|
|
task.callback = this
|
|
task.params = Pair(actionType, item)
|
|
invokeBeforeExecute(task)
|
|
|
|
val result = ManualTaskStarter.invokeExecute(task)
|
|
invokeAfterExecute(task, result)
|
|
|
|
if (!result.succeed) {
|
|
contentResolver.insert(Drafts.CONTENT_URI_NOTIFICATIONS, configure(ContentValues()) {
|
|
put(BaseColumns._ID, result.draftId)
|
|
})
|
|
}
|
|
for (status in result.statuses) {
|
|
if (status == null) continue
|
|
val event = TweetEvent.create(context, status, TimelineType.OTHER)
|
|
event.action = TweetEvent.Action.TWEET
|
|
if (item.in_reply_to_status != null && item.in_reply_to_status.user_is_protected) {
|
|
event.inReplyToId = item.in_reply_to_status.id
|
|
}
|
|
HotMobiLogger.getInstance(context).log(status.account_key, event)
|
|
}
|
|
}
|
|
if (preferences.getBoolean(KEY_REFRESH_AFTER_TWEET)) {
|
|
handler.post { twitterWrapper.refreshAll() }
|
|
}
|
|
stopForeground(false)
|
|
notificationManager.cancel(NOTIFICATION_ID_UPDATE_STATUS)
|
|
}
|
|
|
|
|
|
private fun sendDirectMessage(builder: NotificationCompat.Builder,
|
|
accountKey: UserKey,
|
|
recipientId: String,
|
|
text: String,
|
|
imageUri: String?): SingleResponse<ParcelableDirectMessage> {
|
|
val details = AccountUtils.getAccountDetails(AccountManager.get(this),
|
|
accountKey, true) ?: return SingleResponse.getInstance()
|
|
val twitter = details.newMicroBlogInstance(context = this, cls = MicroBlog::class.java)
|
|
val twitterUpload = details.newMicroBlogInstance(context = this, cls = TwitterUpload::class.java)
|
|
try {
|
|
val directMessage: ParcelableDirectMessage
|
|
when (details.type) {
|
|
AccountType.FANFOU -> {
|
|
if (imageUri != null) {
|
|
throw MicroBlogException("Can't send image DM on Fanfou")
|
|
}
|
|
val dm = twitter.sendFanfouDirectMessage(recipientId, text)
|
|
directMessage = ParcelableDirectMessageUtils.fromDirectMessage(dm, accountKey, true)
|
|
}
|
|
else -> {
|
|
if (imageUri != null) {
|
|
val mediaUri = Uri.parse(imageUri)
|
|
val listener = MessageMediaUploadListener(this, notificationManager,
|
|
builder, text)
|
|
val chucked = details.type == AccountType.TWITTER
|
|
val uploadResp = UpdateStatusTask.getBodyFromMedia(this, mediaLoader,
|
|
mediaUri, null, ParcelableMedia.Type.IMAGE, chucked, listener).use { body ->
|
|
val resp = uploadMedia(twitterUpload, body.body)
|
|
body.deleteOnSuccess?.forEach { item ->
|
|
item.delete(this)
|
|
}
|
|
return@use resp
|
|
}
|
|
val response = twitter.sendDirectMessage(recipientId,
|
|
text, uploadResp.id)
|
|
directMessage = ParcelableDirectMessageUtils.fromDirectMessage(response,
|
|
accountKey, true)
|
|
} else {
|
|
val response = twitter.sendDirectMessage(recipientId, text)
|
|
directMessage = ParcelableDirectMessageUtils.fromDirectMessage(response,
|
|
accountKey, true)
|
|
}
|
|
}
|
|
}
|
|
Utils.setLastSeen(this, UserKey(recipientId, accountKey.host),
|
|
System.currentTimeMillis())
|
|
|
|
return SingleResponse(directMessage)
|
|
} catch (e: IOException) {
|
|
return SingleResponse(e)
|
|
} catch (e: MicroBlogException) {
|
|
return SingleResponse(e)
|
|
}
|
|
|
|
}
|
|
|
|
|
|
@Throws(IOException::class, MicroBlogException::class)
|
|
private fun uploadMedia(upload: TwitterUpload, body: Body): MediaUploadResponse {
|
|
val mediaType = body.contentType().contentType
|
|
val length = body.length()
|
|
val stream = body.stream()
|
|
var response = upload.initUploadMedia(mediaType, length, null)
|
|
val segments = if (length == 0L) 0 else (length / BULK_SIZE + 1).toInt()
|
|
for (segmentIndex in 0..segments - 1) {
|
|
val currentBulkSize = Math.min(BULK_SIZE, length - segmentIndex * BULK_SIZE).toInt()
|
|
val bulk = SimpleBody(ContentType.OCTET_STREAM, null, currentBulkSize.toLong(),
|
|
stream)
|
|
upload.appendUploadMedia(response.id, segmentIndex, bulk)
|
|
}
|
|
response = upload.finalizeUploadMedia(response.id)
|
|
run {
|
|
var info: ProcessingInfo? = response.processingInfo
|
|
while (info != null && shouldWaitForProcess(info)) {
|
|
val checkAfterSecs = info.checkAfterSecs
|
|
if (checkAfterSecs <= 0) {
|
|
break
|
|
}
|
|
try {
|
|
Thread.sleep(TimeUnit.SECONDS.toMillis(checkAfterSecs))
|
|
} catch (e: InterruptedException) {
|
|
break
|
|
}
|
|
|
|
response = upload.getUploadMediaStatus(response.id)
|
|
info = response.processingInfo
|
|
}
|
|
}
|
|
val info = response.processingInfo
|
|
if (info != null && ProcessingInfo.State.FAILED == info.state) {
|
|
val exception = MicroBlogException()
|
|
val errorInfo = info.error
|
|
if (errorInfo != null) {
|
|
exception.errors = arrayOf(errorInfo)
|
|
}
|
|
throw exception
|
|
}
|
|
return response
|
|
}
|
|
|
|
private fun shouldWaitForProcess(info: ProcessingInfo): Boolean {
|
|
when (info.state) {
|
|
ProcessingInfo.State.PENDING, ProcessingInfo.State.IN_PROGRESS -> return true
|
|
else -> return false
|
|
}
|
|
}
|
|
|
|
private fun <T> performStatusAction(draft: Draft, action: (accountKey: UserKey, status: ParcelableStatus) -> AbstractTask<*, T, *>): Boolean {
|
|
val accountKey = draft.account_keys?.firstOrNull() ?: return false
|
|
val status = (draft.action_extras as? StatusObjectExtras)?.status ?: return false
|
|
val task = action(accountKey, status)
|
|
invokeBeforeExecute(task)
|
|
val result = ManualTaskStarter.invokeExecute(task)
|
|
invokeAfterExecute(task, result)
|
|
return true
|
|
}
|
|
|
|
private fun invokeBeforeExecute(task: AbstractTask<*, *, *>) {
|
|
handler.post { ManualTaskStarter.invokeBeforeExecute(task) }
|
|
}
|
|
|
|
private fun <T> invokeAfterExecute(task: AbstractTask<*, T, *>, result: T) {
|
|
handler.post { ManualTaskStarter.invokeAfterExecute(task, result) }
|
|
}
|
|
|
|
internal class MessageMediaUploadListener(private val context: Context, private val manager: NotificationManagerWrapper,
|
|
builder: NotificationCompat.Builder, private val message: String) : ReadListener {
|
|
|
|
var percent: Int = 0
|
|
|
|
private val builder: Builder
|
|
|
|
init {
|
|
this.builder = builder
|
|
}
|
|
|
|
override fun onRead(length: Long, position: Long) {
|
|
val percent = if (length > 0) (position * 100 / length).toInt() else 0
|
|
if (this.percent != percent) {
|
|
manager.notify(NOTIFICATION_ID_SEND_DIRECT_MESSAGE,
|
|
updateSendDirectMessageNotification(context, builder, percent, message))
|
|
}
|
|
this.percent = percent
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private val BULK_SIZE = (128 * 1024).toLong() // 128KiB
|
|
|
|
private fun updateSendDirectMessageNotification(context: Context,
|
|
builder: NotificationCompat.Builder,
|
|
progress: Int, message: String?): Notification {
|
|
builder.setContentTitle(context.getString(R.string.sending_direct_message))
|
|
if (message != null) {
|
|
builder.setContentText(message)
|
|
}
|
|
builder.setSmallIcon(R.drawable.ic_stat_send)
|
|
builder.setProgress(100, progress, progress >= 100 || progress <= 0)
|
|
builder.setOngoing(true)
|
|
return builder.build()
|
|
}
|
|
|
|
private fun updateUpdateStatusNotification(context: Context,
|
|
builder: NotificationCompat.Builder,
|
|
progress: Int,
|
|
status: ParcelableStatusUpdate?): Notification {
|
|
builder.setContentTitle(context.getString(R.string.updating_status_notification))
|
|
if (status != null) {
|
|
builder.setContentText(status.text)
|
|
}
|
|
builder.setSmallIcon(R.drawable.ic_stat_send)
|
|
builder.setProgress(100, progress, progress >= 100 || progress <= 0)
|
|
builder.setOngoing(true)
|
|
return builder.build()
|
|
}
|
|
|
|
fun updateStatusesAsync(context: Context, @Draft.Action action: String,
|
|
vararg statuses: ParcelableStatusUpdate) {
|
|
val intent = Intent(context, LengthyOperationsService::class.java)
|
|
intent.action = INTENT_ACTION_UPDATE_STATUS
|
|
intent.putExtra(EXTRA_STATUSES, statuses)
|
|
intent.putExtra(EXTRA_ACTION, action)
|
|
context.startService(intent)
|
|
}
|
|
}
|
|
|
|
}
|