在main.xml中:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_horizontal"
android:background="#00ff33">
<EditText
android:id="@+id/phonenumber"
android:layout_margin="8dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/setnumber"
android:layout_width="80dp"
android:layout_height="40dp"
android:textColor="#ffffff"
android:background="#3399ff"
android:text="过滤"/>
<Button
android:id="@+id/cancelnumber"
android:layout_marginLeft="20dp"
android:layout_width="80dp"
android:layout_height="40dp"
android:textColor="#ffffff"
android:background="#3399ff"
android:text="取消过滤"/>
</LinearLayout>
</LinearLayout>
在IService.java中:
package com.li.phone;
public interface IService {
}
在PhoneBroadcastReceiver.java中:
package com.li.phone;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class PhoneBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_NEW_OUTGOING_CALL.equals(intent.getAction())) { // 去电
String outgoingNumber = intent
.getStringExtra(Intent.EXTRA_PHONE_NUMBER); // 去电号码
Intent pit = new Intent(context, PhoneService.class);
pit.putExtra("outgoingNumber", outgoingNumber);
context.startService(pit);
} else { // 来电
context.startService(new Intent(context, PhoneService.class));
}
}
}
在PhoneService.java中:
package com.li.phone;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Binder;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
public class PhoneService extends Service {
private TelephonyManager telephony = null;
private AudioManager audio = null; // 声音服务
private String phoneNumber = null; // 要过滤的电话
private IBinder myBinder = new BinderImpl();
class BinderImpl extends Binder implements IService {
@Override
public String getInterfaceDescriptor() {
return "过滤电话“" + PhoneService.this.phoneNumber + "”设置成功!";
}
}
@Override
public IBinder onBind(Intent intent) {
this.phoneNumber = intent.getStringExtra("phonenumber"); // 取得电话号码
this.audio = (AudioManager) super
.getSystemService(Context.AUDIO_SERVICE); // 声音服务
this.telephony = (TelephonyManager) super
.getSystemService(Context.TELEPHONY_SERVICE);
this.telephony.listen(new PhoneStateListenerImpl(),
PhoneStateListener.LISTEN_CALL_STATE); // 设置监听操作
return this.myBinder;
}
private class PhoneStateListenerImpl extends PhoneStateListener {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_IDLE: // 挂断电话
PhoneService.this.audio
.setRingerMode(AudioManager.RINGER_MODE_NORMAL); // 正常音
break;
case TelephonyManager.CALL_STATE_RINGING: // 领音响起
if (incomingNumber.equals(PhoneService.this.phoneNumber)) { // 电话号码匹配
PhoneService.this.audio
.setRingerMode(AudioManager.RINGER_MODE_SILENT); // 静音
}
break;
case TelephonyManager.CALL_STATE_OFFHOOK: // 接听电话
break;
}
}
}
}
在MyPhoneDemo.java中:
package com.li.phone;
import com.li.phone.PhoneService.BinderImpl;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MyPhoneDemo extends Activity {
private EditText phoneNumber = null ;
private Button setNumber = null ;
private Button cancelNumber = null ;
private IService service = null ;
private ServiceConnectionImpl serviceConnection = new ServiceConnectionImpl() ;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.setContentView(R.layout.main);
this.phoneNumber = (EditText) super.findViewById(R.id.phonenumber) ;
this.setNumber = (Button) super.findViewById(R.id.setnumber) ;
this.cancelNumber = (Button) super.findViewById(R.id.cancelnumber) ;
this.setNumber.setOnClickListener(new SetOnClickListenerImpl()) ;
this.cancelNumber.setOnClickListener(new CancelOnClickListenerImpl()) ;
}
private class SetOnClickListenerImpl implements OnClickListener {
public void onClick(View v) {
Intent intent = new Intent(MyPhoneDemo.this,PhoneService.class) ;
intent.putExtra("phonenumber", MyPhoneDemo.this.phoneNumber
.getText().toString());
MyPhoneDemo.this.bindService(intent,
MyPhoneDemo.this.serviceConnection,
Context.BIND_AUTO_CREATE);
}
}
private class CancelOnClickListenerImpl implements OnClickListener {
public void onClick(View v) {
if(MyPhoneDemo.this.service != null) {
MyPhoneDemo.this.unbindService(MyPhoneDemo.this.serviceConnection) ;
MyPhoneDemo.this.stopService(new Intent(MyPhoneDemo.this,PhoneService.class)) ;
Toast.makeText(MyPhoneDemo.this, "黑名单已取消", Toast.LENGTH_LONG)
.show();
MyPhoneDemo.this.service = null ;
}
}
}
private class ServiceConnectionImpl implements ServiceConnection {
public void onServiceConnected(ComponentName name, IBinder service) {
MyPhoneDemo.this.service = (BinderImpl) service ;
try {
Toast.makeText(MyPhoneDemo.this, service.getInterfaceDescriptor(), Toast.LENGTH_LONG).show() ;
} catch (RemoteException e) {
}
}
public void onServiceDisconnected(ComponentName name) {
}
}
}
修改AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.li.phone"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="15" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SEND_SMS"></uses-permission>
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MyPhoneDemo"
android:label="@string/title_activity_my_phone_demo" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".PhoneService" />
<receiver android:name=".PhoneBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
</application>
</manifest>
准备一张名为ball的小球图片。
在main.xml中:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.li.sensor.BallView
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>
在BallView.java中:
package com.li.sensor;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.AttributeSet;
import android.view.View;
public class BallView extends View implements SensorEventListener {
private Bitmap ball = null;
private float[] allValue;
private Point point = new Point();
private int xSpeed = 0;
private int ySpeed = 0;
public BallView(Context context, AttributeSet attrs) {
super(context, attrs);
super.setBackgroundColor(Color.GREEN); // 底色为绿色
this.ball = BitmapFactory.decodeResource(super.getResources(),
R.drawable.ball);
SensorManager manager = (SensorManager) context
.getSystemService(Context.SENSOR_SERVICE); // 现在只是找到了一个传感器,但是没有定义类型
manager.registerListener(this,
manager.getDefaultSensor(Sensor.TYPE_ORIENTATION),
SensorManager.SENSOR_DELAY_GAME); // 创建了一个适合于游戏操作的方位传感器
}
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
public void onSensorChanged(SensorEvent event) { // 传感器方位改变
if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { // 现在是方位传感器
float value[] = event.values; // 取得所有的偏离数据
BallView.this.allValue = value; // 取得三个轴的值
super.postInvalidate(); // 主线程的现实需要重绘
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint p = new Paint(); // 根据传感器的数值来改变球的速度
if (this.allValue != null) { // 已经取得了数据
this.xSpeed = (int) -this.allValue[2]; // 计算X轴速度
this.ySpeed = (int) -this.allValue[1];
}
this.point.x += this.xSpeed;
this.point.y += this.ySpeed;
if (this.point.x < 0) {
this.point.x = 0;
}
if (this.point.y < 0) {
this.point.y = 0;
}
if (point.x > super.getWidth() - this.ball.getWidth()) { // X轴已经显示过了
this.point.x = super.getWidth() - this.ball.getWidth();
}
if (point.y > super.getHeight() - this.ball.getHeight()) {
this.point.y = super.getHeight() - this.ball.getWidth(); // 设置Y 轴的边界
}
canvas.drawBitmap(this.ball, this.point.x, this.point.y, p);
}
}
在MySensorDemo.java中:
package com.li.sensor;
import android.app.Activity;
import android.os.Bundle;
public class MySensorDemo extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
修改AndroidManifest.xml,设置为竖屏。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.li.sensor"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="15" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MySensorDemo"
android:label="@string/title_activity_my_sensor_demo"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
QueueFile.java 文件队列说明解说
/**
* Copyright (C) 2010 Square, Inc.
*
* 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 com.squareup.util;
import com.squareup.Square;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.NoSuchElementException;
/**
* A reliable, efficient, file-based, FIFO queue. Additions and removals are
* O(1). All operations are atomic. Writes are synchronous; data will be
* written to disk before an operation returns. The underlying file is
* structured to survive process and even system crashes. If an I/O exception
* is thrown during a mutating change, the change is aborted. It is safe to
* continue to use a {@code QueueFile} instance after an exception.
*
* <p>All operations are synchronized. In a traditional queue, the remove
* operation returns an element. In this queue, {@link #peek} and {@link
* #remove} are used in conjunction. Use {@code peek} to retrieve the first
* element, and then {@code remove} to remove it after successful processing.
* If the system crashes after {@code peek} and during processing, the element
* will remain in the queue, to be processed when the system restarts.
*
* <p><b><font color="red">NOTE:</font></b> The current implementation is
* built for file systems that support atomic segment writes (like YAFFS).
* Most conventional file systems don't support this; if the power goes out
* while writing a segment, the segment will contain garbage and the file will
* be corrupt. We'll add journaling support so this class can be used with
* more file systems later.
*
* @author Bob Lee (bob@squareup.com)
*/
public class QueueFile {
/** Initial file size in bytes. */
private static final int INITIAL_LENGTH = 4096; // one file system block
/** Length of header in bytes. */
static final int HEADER_LENGTH = 16;
/**
* The underlying file. Uses a ring buffer to store entries. Designed so
* that a modification isn't committed or visible until we write the header.
* The header is much smaller than a segment. So long as the underlying file
* system supports atomic segment writes, changes to the queue are atomic.
* Storing the file length ensures we can recover from a failed expansion
* (i.e. if setting the file length succeeds but the process dies before the
* data can be copied).
*
* <pre>
* Format:
* Header (16 bytes)
* Element Ring Buffer (File Length - 16 bytes)
*
* Header:
* File Length (4 bytes)
* Element Count (4 bytes)
* First Element Position (4 bytes, =0 if null)
* Last Element Position (4 bytes, =0 if null)
*
* Element:
* Length (4 bytes)
* Data (Length bytes)
* </pre>
*/
private final RandomAccessFile raf;
/** Cached file length. Always a power of 2. */
int fileLength;
/** Number of elements. */
private int elementCount;
/** Pointer to first (or eldest) element. */
private Element first;
/** Pointer to last (or newest) element. */
private Element last;
/** In-memory buffer. Big enough to hold the header. */
private final byte[] buffer = new byte[16];
/**
* Constructs a new queue backed by the given file. Only one {@code QueueFile}
* instance should access a given file at a time.
*/
public QueueFile(File file) throws IOException {
if (!file.exists()) initialize(file);
raf = open(file);
readHeader();
}
/** For testing. */
QueueFile(RandomAccessFile raf) throws IOException {
this.raf = raf;
readHeader();
}
/**
* Stores int in buffer. The behavior is equivalent to calling
* {@link RandomAccessFile#writeInt}.
*/
private static void writeInt(byte[] buffer, int offset, int value) {
buffer[offset] = (byte) (value >> 24);
buffer[offset + 1] = (byte) (value >> 16);
buffer[offset + 2] = (byte) (value >> 8);
buffer[offset + 3] = (byte) value;
}
/**
* Stores int values in buffer. The behavior is equivalent to calling
* {@link RandomAccessFile#writeInt} for each value.
*/
private static void writeInts(byte[] buffer, int... values) {
int offset = 0;
for (int value : values) {
writeInt(buffer, offset, value);
offset += 4;
}
}
/**
* Reads an int from a byte[].
*/
private static int readInt(byte[] buffer, int offset) {
return ((buffer[offset] & 0xff) << 24)
+ ((buffer[offset + 1] & 0xff) << 16)
+ ((buffer[offset + 2] & 0xff) << 8)
+ (buffer[offset + 3] & 0xff);
}
/**
* Reads the header.
*/
private void readHeader() throws IOException {
raf.seek(0);
raf.readFully(buffer);
fileLength = readInt(buffer, 0);
elementCount = readInt(buffer, 4);
int firstOffset = readInt(buffer, 8);
int lastOffset = readInt(buffer, 12);
first = readElement(firstOffset);
last = readElement(lastOffset);
}
/**
* Writes header atomically. The arguments contain the updated values. The
* class member fields should not have changed yet. This only updates the
* state in the file. It's up to the caller to update the class member
* variables *after* this call succeeds. Assumes segment writes are atomic
* in the underlying file system.
*/
private void writeHeader(int fileLength, int elementCount, int firstPosition,
int lastPosition) throws IOException {
writeInts(buffer, fileLength, elementCount, firstPosition, lastPosition);
raf.seek(0);
raf.write(buffer);
}
/**
* Returns the Element for the given offset.
*/
private Element readElement(int position) throws IOException {
if (position == 0) return Element.NULL;
raf.seek(position);
return new Element(position, raf.readInt());
}
/** Atomically initializes a new file. */
private static void initialize(File file) throws IOException {
// Use a temp file so we don't leave a partially-initialized file.
File tempFile = new File(file.getPath() + ".tmp");
RandomAccessFile raf = open(tempFile);
try {
raf.setLength(INITIAL_LENGTH);
raf.seek(0);
byte[] headerBuffer = new byte[16];
writeInts(headerBuffer, INITIAL_LENGTH, 0, 0, 0);
raf.write(headerBuffer);
} finally {
raf.close();
}
// A rename is atomic.
if (!tempFile.renameTo(file)) throw new IOException("Rename failed!");
}
/**
* Opens a random access file that writes synchronously.
*/
private static RandomAccessFile open(File file) throws FileNotFoundException {
return new RandomAccessFile(file, "rwd");
}
/**
* Wraps the position if it exceeds the end of the file.
*/
private int wrapPosition(int position) {
return position < fileLength ? position
: HEADER_LENGTH + position - fileLength;
}
/**
* Writes count bytes from buffer to position in file. Automatically wraps
* write if position is past the end of the file or if buffer overlaps it.
*
* @param position in file to write to
* @param buffer to write from
* @param count # of bytes to write
*/
private void ringWrite(int position, byte[] buffer, int offset, int count)
throws IOException {
position = wrapPosition(position);
if (position + count <= fileLength) {
raf.seek(position);
raf.write(buffer, offset, count);
} else {
// The write overlaps the EOF.
// # of bytes to write before the EOF.
int beforeEof = fileLength - position;
raf.seek(position);
raf.write(buffer, offset, beforeEof);
raf.seek(HEADER_LENGTH);
raf.write(buffer, offset + beforeEof, count - beforeEof);
}
}
/**
* Reads count bytes into buffer from file. Wraps if necessary.
*
* @param position in file to read from
* @param buffer to read into
* @param count # of bytes to read
*/
private void ringRead(int position, byte[] buffer, int offset, int count)
throws IOException {
position = wrapPosition(position);
if (position + count <= fileLength) {
raf.seek(position);
raf.readFully(buffer, 0, count);
} else {
// The read overlaps the EOF.
// # of bytes to read before the EOF.
int beforeEof = fileLength - position;
raf.seek(position);
raf.readFully(buffer, offset, beforeEof);
raf.seek(HEADER_LENGTH);
raf.readFully(buffer, offset + beforeEof, count - beforeEof);
}
}
/**
* Adds an element to the end of the queue.
*
* @param data to copy bytes from
*/
public void add(byte[] data) throws IOException {
add(data, 0, data.length);
}
/**
* Adds an element to the end of the queue.
*
* @param data to copy bytes from
* @param offset to start from in buffer
* @param count number of bytes to copy
*
* @throws IndexOutOfBoundsException if {@code offset < 0} or
* {@code count < 0}, or if {@code offset + count} is bigger than the length
* of {@code buffer}.
*/
public synchronized void add(byte[] data, int offset, int count)
throws IOException {
Objects.nonNull(data, "buffer");
if ((offset | count) < 0 || count > data.length - offset) {
throw new IndexOutOfBoundsException();
}
expandIfNecessary(count);
// Insert a new element after the current last element.
boolean wasEmpty = isEmpty();
int position = wasEmpty ? HEADER_LENGTH : wrapPosition(
last.position + Element.HEADER_LENGTH + last.length);
Element newLast = new Element(position, count);
// Write length.
writeInt(buffer, 0, count);
ringWrite(newLast.position, buffer, 0, Element.HEADER_LENGTH);
// Write data.
ringWrite(newLast.position + Element.HEADER_LENGTH, data, offset, count);
// Commit the addition. If wasEmpty, first == last.
int firstPosition = wasEmpty ? newLast.position : first.position;
writeHeader(fileLength, elementCount + 1, firstPosition, newLast.position);
last = newLast;
elementCount++;
if (wasEmpty) first = last; // first element
}
/**
* Returns the number of used bytes.
*/
private int usedBytes() {
if (elementCount == 0) return HEADER_LENGTH;
if (last.position >= first.position) {
// Contiguous queue.
return (last.position - first.position) // all but last entry
+ Element.HEADER_LENGTH + last.length // last entry
+ HEADER_LENGTH;
} else {
// tail < head. The queue wraps.
return last.position // buffer front + header
+ Element.HEADER_LENGTH + last.length // last entry
+ fileLength - first.position; // buffer end
}
}
/**
* Returns number of unused bytes.
*/
private int remainingBytes() {
return fileLength - usedBytes();
}
/**
* Returns true if this queue contains no entries.
*/
public synchronized boolean isEmpty() {
return elementCount == 0;
}
/**
* If necessary, expands the file to accommodate an additional element of the
* given length.
*
* @param dataLength length of data being added
*/
private void expandIfNecessary(int dataLength) throws IOException {
int elementLength = Element.HEADER_LENGTH + dataLength;
int remainingBytes = remainingBytes();
if (remainingBytes >= elementLength) return;
// Expand.
int previousLength = fileLength;
int newLength;
// Double the length until we can fit the new data.
do {
remainingBytes += previousLength;
newLength = previousLength << 1;
previousLength = newLength;
} while (remainingBytes < elementLength);
raf.setLength(newLength);
// If the buffer is split, we need to make it contiguous.
if (last.position < first.position) {
FileChannel channel = raf.getChannel();
channel.position(fileLength); // destination position
int count = last.position + Element.HEADER_LENGTH + last.length
- HEADER_LENGTH;
if (channel.transferTo(HEADER_LENGTH, count, channel) != count) {
throw new AssertionError("Copied insufficient number of bytes!");
}
// Commit the expansion.
int newLastPosition = fileLength + last.position - HEADER_LENGTH;
writeHeader(newLength, elementCount, first.position, newLastPosition);
last = new Element(newLastPosition, last.length);
} else {
writeHeader(newLength, elementCount, first.position, last.position);
}
fileLength = newLength;
}
/**
* Reads the eldest element. Returns null if the queue is empty.
*/
public synchronized byte[] peek() throws IOException {
if (isEmpty()) return null;
int length = first.length;
byte[] data = new byte[length];
ringRead(first.position + Element.HEADER_LENGTH, data, 0, length);
return data;
}
/**
* Invokes reader with the eldest element, if an element is available.
*/
public synchronized void peek(ElementReader reader) throws IOException {
if (elementCount > 0) {
reader.read(new ElementInputStream(first), first.length);
}
}
/**
* Invokes the given reader once for each element in the queue, from
* eldest to most recently added.
*/
public synchronized void forEach(ElementReader reader) throws IOException {
int position = first.position;
for (int i = 0; i < elementCount; i++) {
Element current = readElement(position);
reader.read(new ElementInputStream(current), current.length);
position = wrapPosition(current.position + Element.HEADER_LENGTH
+ current.length);
}
}
/**
* Reads a single element.
*/
private class ElementInputStream extends InputStream {
private int position;
private int remaining;
private ElementInputStream(Element element) {
position = wrapPosition(element.position + Element.HEADER_LENGTH);
remaining = element.length;
}
@Override public int read(byte[] buffer, int offset, int length)
throws IOException {
Objects.nonNull(buffer, "buffer");
if ((offset | length) < 0 || length > buffer.length - offset) {
throw new ArrayIndexOutOfBoundsException();
}
if (length > remaining) length = remaining;
ringRead(position, buffer, offset, length);
position = wrapPosition(position + length);
remaining -= length;
return length;
}
@Override public int read() throws IOException {
if (remaining == 0) return -1;
raf.seek(position);
int b = raf.read();
position = wrapPosition(position + 1);
remaining--;
return b;
}
}
/**
* Returns the number of elements in this queue.
*/
public synchronized int size() {
return elementCount;
}
/**
* Removes the eldest element.
*
* @throw NoSuchElementException if the queue is empty
*/
public synchronized void remove() throws IOException {
if (isEmpty()) throw new NoSuchElementException();
if (elementCount == 1) {
clear();
} else {
// assert elementCount > 1
int newFirstPosition = wrapPosition(first.position
+ Element.HEADER_LENGTH + first.length);
ringRead(newFirstPosition, buffer, 0, Element.HEADER_LENGTH);
int length = readInt(buffer, 0);
writeHeader(fileLength, elementCount - 1, newFirstPosition, last.position);
elementCount--;
first = new Element(newFirstPosition, length);
}
}
/**
* Clears this queue. Truncates the file to the initial size.
*/
public synchronized void clear() throws IOException {
if (fileLength > INITIAL_LENGTH) raf.setLength(INITIAL_LENGTH);
writeHeader(INITIAL_LENGTH, 0, 0, 0);
elementCount = 0;
first = last = Element.NULL;
fileLength = INITIAL_LENGTH;
}
/**
* Closes the underlying file.
*/
public synchronized void close() throws IOException {
raf.close();
}
@Override public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append(getClass().getSimpleName()).append('[');
builder.append("fileLength=").append(fileLength);
builder.append(", size=").append(elementCount);
builder.append(", first=").append(first);
builder.append(", last=").append(last);
builder.append(", element lengths=[");
try {
forEach(new ElementReader() {
boolean first = true;
public void read(InputStream in, int length) throws IOException {
if (first) {
first = false;
} else {
builder.append(", ");
}
builder.append(length);
}
});
} catch (IOException e) {
Square.warning(e);
}
builder.append("]]");
return builder.toString();
}
/** A pointer to an element. */
static class Element {
/** Length of element header in bytes. */
static final int HEADER_LENGTH = 4;
/** Null element. */
static final Element NULL = new Element(0, 0);
/** Position in file. */
final int position;
/** The length of the data. */
final int length;
/**
* Constructs a new element.
*
* @param position within file
* @param length of data
*/
Element(int position, int length) {
this.position = position;
this.length = length;
}
@Override public String toString() {
return getClass().getSimpleName() + "["
+ "position = " + position
+ ", length = " + length + "]";
}
}
/**
* Reads queue elements. Enables partial reads as opposed to reading all
* of the bytes into a byte[].
*/
public interface ElementReader {
/*
* TODO: Support remove() call from read().
*/
/**
* Called once per element.
*
* @param in stream of element data. Reads as many bytes as requested,
* unless fewer than the request number of bytes remains, in which case it
* reads all the remaining bytes.
* @param length of element data in bytes
*/
public void read(InputStream in, int length) throws IOException;
}
}
QueueFileTest.java:
package com.squareup.util;
import android.test.AndroidTestCase;
import com.squareup.Square;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import junit.framework.ComparisonFailure;
/**
* Tests for QueueFile.
*
* @author Bob Lee (bob@squareup.com)
*/
public class QueueFileTest extends AndroidTestCase {
/**
* Takes up 33401 bytes in the queue (N*(N+1)/2+4*N). Picked 254 instead of
* 255 so that the number of bytes isn't a multiple of 4.
*/
private static int N = 254; //
private static byte[][] values = new byte[N][];
static {
for (int i = 0; i < N; i++) {
byte[] value = new byte[i];
// Example: values[3] = { 3, 2, 1 }
for (int ii = 0; ii < i; ii++) value[ii] = (byte) (i - ii);
values[i] = value;
}
}
private File file;
@Override protected void setUp() throws Exception {
file = getContext().getFileStreamPath("test.queue");
file.delete();
}
@Override protected void tearDown() throws Exception {
file.delete();
}
public void testAddOneElement() throws IOException {
// This test ensures that we update 'first' correctly.
QueueFile queue = new QueueFile(file);
byte[] expected = values[253];
queue.add(expected);
assertEquals(expected, queue.peek());
queue.close();
queue = new QueueFile(file);
assertEquals(expected, queue.peek());
}
public void testAddAndRemoveElements() throws IOException {
long start = System.nanoTime();
Queue<byte[]> expected = new LinkedList<byte[]>();
for (int round = 0; round < 5; round++) {
QueueFile queue = new QueueFile(file);
for (int i = 0; i < N; i++) {
queue.add(values[i]);
expected.add(values[i]);
}
// Leave N elements in round N, 15 total for 5 rounds. Removing all the
// elements would be like starting with an empty queue.
for (int i = 0; i < N - round - 1; i++) {
assertEquals(expected.remove(), queue.peek());
queue.remove();
}
queue.close();
}
// Remove and validate remaining 15 elements.
QueueFile queue = new QueueFile(file);
assertEquals(15, queue.size());
assertEquals(expected.size(), queue.size());
while (!expected.isEmpty()) {
assertEquals(expected.remove(), queue.peek());
queue.remove();
}
queue.close();
// length() returns 0, but I checked the size w/ 'ls', and it is correct.
// assertEquals(65536, file.length());
Square.debug("Ran in " + ((System.nanoTime() - start) / 1000000) + "ms.");
}
/**
* Tests queue expansion when the data crosses EOF.
*/
public void testSplitExpansion() throws IOException {
// This should result in 3560 bytes.
int max = 80;
Queue<byte[]> expected = new LinkedList<byte[]>();
QueueFile queue = new QueueFile(file);
for (int i = 0; i < max; i++) {
expected.add(values[i]);
queue.add(values[i]);
}
// Remove all but 1.
for (int i = 1; i < max; i++) {
assertEquals(expected.remove(), queue.peek());
queue.remove();
}
// This should wrap around before expanding.
for (int i = 0; i < N; i++) {
expected.add(values[i]);
queue.add(values[i]);
}
while (!expected.isEmpty()) {
assertEquals(expected.remove(), queue.peek());
queue.remove();
}
queue.close();
}
public void testFailedAdd() throws IOException {
QueueFile queueFile = new QueueFile(file);
queueFile.add(values[253]);
queueFile.close();
final BrokenRandomAccessFile braf = new BrokenRandomAccessFile(file, "rwd");
queueFile = new QueueFile(braf);
try {
queueFile.add(values[252]);
fail();
} catch (IOException e) { /* expected */ }
braf.rejectCommit = false;
// Allow a subsequent add to succeed.
queueFile.add(values[251]);
queueFile.close();
queueFile = new QueueFile(file);
assertEquals(2, queueFile.size());
assertEquals(values[253], queueFile.peek());
queueFile.remove();
assertEquals(values[251], queueFile.peek());
}
public void testFailedRemoval() throws IOException {
QueueFile queueFile = new QueueFile(file);
queueFile.add(values[253]);
queueFile.close();
final BrokenRandomAccessFile braf = new BrokenRandomAccessFile(file, "rwd");
queueFile = new QueueFile(braf);
try {
queueFile.remove();
fail();
} catch (IOException e) { /* expected */ }
queueFile.close();
queueFile = new QueueFile(file);
assertEquals(1, queueFile.size());
assertEquals(values[253], queueFile.peek());
queueFile.add(values[99]);
queueFile.remove();
assertEquals(values[99], queueFile.peek());
}
public void testFailedExpansion() throws IOException {
QueueFile queueFile = new QueueFile(file);
queueFile.add(values[253]);
queueFile.close();
final BrokenRandomAccessFile braf = new BrokenRandomAccessFile(file, "rwd");
queueFile = new QueueFile(braf);
try {
// This should trigger an expansion which should fail.
queueFile.add(new byte[8000]);
fail();
} catch (IOException e) { /* expected */ }
queueFile.close();
queueFile = new QueueFile(file);
assertEquals(1, queueFile.size());
assertEquals(values[253], queueFile.peek());
assertEquals(4096, queueFile.fileLength);
queueFile.add(values[99]);
queueFile.remove();
assertEquals(values[99], queueFile.peek());
}
public void testPeakWithElementReader() throws IOException {
QueueFile queueFile = new QueueFile(file);
final byte[] a = { 1, 2 };
queueFile.add(a);
final byte[] b = { 3, 4, 5 };
queueFile.add(b);
queueFile.peek(new QueueFile.ElementReader() {
public void read(InputStream in, int length) throws IOException {
assertEquals(length, 2);
byte[] actual = new byte[length];
in.read(actual);
assertEquals(a, actual);
}
});
queueFile.peek(new QueueFile.ElementReader() {
public void read(InputStream in, int length) throws IOException {
assertEquals(length, 2);
assertEquals(1, in.read());
assertEquals(2, in.read());
assertEquals(-1, in.read());
}
});
queueFile.remove();
queueFile.peek(new QueueFile.ElementReader() {
public void read(InputStream in, int length) throws IOException {
assertEquals(length, 3);
byte[] actual = new byte[length];
in.read(actual);
assertEquals(b, actual);
}
});
assertEquals(b, queueFile.peek());
assertEquals(1, queueFile.size());
}
public void testForEach() throws IOException {
QueueFile queueFile = new QueueFile(file);
final byte[] a = { 1, 2 };
queueFile.add(a);
final byte[] b = { 3, 4, 5 };
queueFile.add(b);
final int[] iteration = new int[] { 0 };
QueueFile.ElementReader elementReader = new QueueFile.ElementReader() {
public void read(InputStream in, int length) throws IOException {
if (iteration[0] == 0) {
assertEquals(length, 2);
byte[] actual = new byte[length];
in.read(actual);
assertEquals(a, actual);
} else if (iteration[0] == 1) {
assertEquals(length, 3);
byte[] actual = new byte[length];
in.read(actual);
assertEquals(b, actual);
} else {
fail();
}
iteration[0]++;
}
};
queueFile.forEach(elementReader);
assertEquals(a, queueFile.peek());
assertEquals(2, iteration[0]);
}
/**
* Compares two byte[]s for equality.
*/
private static void assertEquals(byte[] expected, byte[] actual) {
if (!Arrays.equals(expected, actual)) {
throw new ComparisonFailure(null, Arrays.toString(expected),
Arrays.toString(actual));
}
}
/**
* A RandomAccessFile that can break when you go to write the COMMITTED
* status.
*/
static class BrokenRandomAccessFile extends RandomAccessFile {
boolean rejectCommit = true;
BrokenRandomAccessFile(File file, String mode)
throws FileNotFoundException {
super(file, mode);
}
@Override public void write(byte[] buffer) throws IOException {
if (rejectCommit && getFilePointer() == 0) {
throw new IOException("No commit for you!");
}
super.write(buffer);
}
}
}