/*
* Copyright 2013 wada811<at.wada811@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.wada811.widget;
import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import at.wada811.utils.LogUtils;
import at.wada811.widget.Toaster.ToasterCallback;
import java.util.ArrayList;
public class ToasterService extends Service {
public static final String TAG = ToasterService.class.getSimpleName();
private static final int MAX_BREADS = 50;
private static final int MESSAGE_TIMEOUT = 2;
final IBinder mForegroundToken = new Binder();
private WorkerHandler mHandler;
private ArrayList<BreadRecord> mBreadQueue;
//サービスに接続するためのBinder
public class ToasterServiceBinder extends Binder {
//サービスの取得
ToasterService getService(){
return ToasterService.this;
}
}
//Binderの生成
private final IBinder mBinder = new ToasterServiceBinder();
@Override
public IBinder onBind(Intent intent){
mBreadQueue = new ArrayList<BreadRecord>();
mHandler = new WorkerHandler();
return mBinder;
}
@Override
public boolean onUnbind(Intent intent){
LogUtils.d();
return super.onUnbind(intent);
}
public static final class BreadRecord {
final ToasterCallback callback;
int duration;
public BreadRecord(ToasterCallback callback, int duration) {
this.callback = callback;
this.duration = duration;
}
public void update(int duration){
this.duration = duration;
}
@Override
public final String toString(){
return "BreadRecord{" + Integer.toHexString(System.identityHashCode(this)) + " callback=" + callback + " duration=" + duration;
}
}
public void enqueueBread(ToasterCallback callback, int duration){
LogUtils.i(TAG, "enqueueBread callback=" + callback + " duration=" + duration);
if(callback == null){
LogUtils.e(TAG, "Not doing toast. callback=" + callback);
return;
}
synchronized(mBreadQueue){
long callingId = Binder.clearCallingIdentity();
try{
BreadRecord record;
int index = indexOfBreadLocked(callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
if(index >= 0){
record = mBreadQueue.get(index);
record.update(duration);
}else{
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
int count = 0;
final int N = mBreadQueue.size();
for(int i = 0; i < N; i++){
count++;
if(count >= MAX_BREADS){
LogUtils.e(TAG, "Package has already posted " + count + " toasts. Not showing more.");
return;
}
}
record = new BreadRecord(callback, duration);
mBreadQueue.add(record);
index = mBreadQueue.size() - 1;
}
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
if(index == 0){
showNextBreadLocked();
}
}finally{
Binder.restoreCallingIdentity(callingId);
}
}
}
public void cancelAllBread(){
LogUtils.i(TAG, "cancelAllBread");
synchronized(mBreadQueue){
for(BreadRecord record : mBreadQueue){
cancelBread(record.callback);
}
}
}
public void cancelBread(ToasterCallback callback){
LogUtils.i(TAG, "cancelBread callback=" + callback);
if(callback == null){
LogUtils.e(TAG, "Not cancelling notification. callback=" + callback);
return;
}
synchronized(mBreadQueue){
long callingId = Binder.clearCallingIdentity();
try{
int index = indexOfBreadLocked(callback);
if(index >= 0){
cancelBreadLocked(index);
}else{
LogUtils.w(TAG, "Bread already cancelled. callback=" + callback);
}
}finally{
Binder.restoreCallingIdentity(callingId);
}
}
}
private void showNextBreadLocked(){
BreadRecord record = mBreadQueue.get(0);
while(record != null){
LogUtils.d(TAG, "Show callback=" + record.callback);
try{
record.callback.show();
scheduleTimeoutLocked(record, false);
return;
}catch(/* Remote */Exception e){
LogUtils.w(TAG, "Object died trying to show notification " + record.callback);
// remove it from the list and let the process die
int index = mBreadQueue.indexOf(record);
if(index >= 0){
mBreadQueue.remove(index);
}
if(mBreadQueue.size() > 0){
record = mBreadQueue.get(0);
}else{
record = null;
}
}
}
}
private void cancelBreadLocked(int index){
BreadRecord record = mBreadQueue.get(index);
try{
record.callback.hide();
}catch(/* Remote */Exception e){
LogUtils.w(TAG, "Object died trying to hide notification " + record.callback);
// don't worry about this, we're about to remove it from the list anyway
}
mBreadQueue.remove(index);
if(mBreadQueue.size() > 0){
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextBreadLocked();
}
}
private void scheduleTimeoutLocked(BreadRecord record, boolean immediate){
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, record);
long delay = immediate ? 0 : record.duration;
mHandler.removeCallbacksAndMessages(record);
if(delay >= 0){
mHandler.sendMessageDelayed(m, delay);
}
}
private void handleTimeout(BreadRecord record){
LogUtils.d(TAG, "Timeout callback=" + record.callback);
synchronized(mBreadQueue){
int index = indexOfBreadLocked(record.callback);
if(index >= 0){
cancelBreadLocked(index);
}
}
}
// lock on mBreadQueue
private int indexOfBreadLocked(ToasterCallback callback){
ArrayList<BreadRecord> list = mBreadQueue;
int len = list.size();
for(int i = 0; i < len; i++){
BreadRecord record = list.get(i);
if(record.callback.equals(callback)){
return i;
}
}
return -1;
}
@SuppressLint("HandlerLeak")
private final class WorkerHandler extends Handler {
@Override
public void handleMessage(Message msg){
switch(msg.what){
case MESSAGE_TIMEOUT:
handleTimeout((BreadRecord)msg.obj);
break;
}
}
}
}