Implementation of the tic-tac-toe sample.
Added some README.txt Change-Id: Ie2703ffdfdeba773d0fd27153ec0cdb806b2e2dc
This commit is contained in:
@@ -19,7 +19,4 @@
|
|||||||
package="com.example.tictactoe.library"
|
package="com.example.tictactoe.library"
|
||||||
android:versionCode="1"
|
android:versionCode="1"
|
||||||
android:versionName="1.0">
|
android:versionName="1.0">
|
||||||
<application>
|
|
||||||
<activity android:name="GameActivity" />
|
|
||||||
</application>
|
|
||||||
</manifest>
|
</manifest>
|
||||||
7
samples/TicTacToeLib/README.txt
Executable file
7
samples/TicTacToeLib/README.txt
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
Sample: TicTacToeLib and TicTacToeMain.
|
||||||
|
|
||||||
|
These two projects work together. They demonstrate how to use the ability to
|
||||||
|
split an APK into multiple projects.
|
||||||
|
|
||||||
|
Please see the README.txt file in ../TicTacToeMain for more details.
|
||||||
|
|
||||||
@@ -25,4 +25,4 @@
|
|||||||
|
|
||||||
android.library=true
|
android.library=true
|
||||||
# Project target.
|
# Project target.
|
||||||
target=android-7
|
target=android-3
|
||||||
|
|||||||
60
samples/TicTacToeLib/res/layout-land/lib_game.xml
Executable file
60
samples/TicTacToeLib/res/layout-land/lib_game.xml
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:gravity="center_vertical|center_horizontal"
|
||||||
|
>
|
||||||
|
|
||||||
|
<com.example.tictactoe.library.GameView
|
||||||
|
android:id="@+id/game_view"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_margin="20dip"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/info_turn"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="10dip"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/next_turn"
|
||||||
|
android:text="I'm done"
|
||||||
|
android:minEms="10"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="20dip"
|
||||||
|
android:layout_marginRight="20dip"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -19,13 +19,13 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="fill_parent"
|
android:layout_height="fill_parent"
|
||||||
android:weightSum="2"
|
android:gravity="center_horizontal"
|
||||||
>
|
>
|
||||||
|
|
||||||
<com.example.tictactoe.library.GameView
|
<com.example.tictactoe.library.GameView
|
||||||
android:id="@+id/game_view"
|
android:id="@+id/game_view"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="fill_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="20dip"
|
android:layout_margin="20dip"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
/>
|
/>
|
||||||
@@ -35,16 +35,16 @@
|
|||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:layout_weight="1"
|
android:layout_marginBottom="10dip"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/info_turn"
|
android:id="@+id/next_turn"
|
||||||
|
android:text="I'm done"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="20dip"
|
android:layout_marginLeft="20dip"
|
||||||
android:layout_marginRight="20dip"
|
android:layout_marginRight="20dip"
|
||||||
android:layout_weight="1"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -16,6 +16,5 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
|
<!-- TODO externalize strings here. -->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,15 +16,36 @@
|
|||||||
|
|
||||||
package com.example.tictactoe.library;
|
package com.example.tictactoe.library;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.os.Handler.Callback;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.example.tictactoe.library.GameView.ICellListener;
|
||||||
|
import com.example.tictactoe.library.GameView.State;
|
||||||
|
|
||||||
|
|
||||||
public class GameActivity extends Activity {
|
public class GameActivity extends Activity {
|
||||||
|
|
||||||
public final static String EXTRA_START_WITH_HUMAN =
|
/** Start player. Must be 1 or 2. Default is 1. */
|
||||||
"com.example.tictactoe.library.GameActivity.EXTRA_START_WITH_HUMAN";
|
public static final String EXTRA_START_PLAYER =
|
||||||
|
"com.example.tictactoe.library.GameActivity.EXTRA_START_PLAYER";
|
||||||
|
|
||||||
private boolean mTurnIsHuman;
|
private static final int MSG_COMPUTER_TURN = 1;
|
||||||
|
private static final long COMPUTER_DELAY_MS = 500;
|
||||||
|
|
||||||
|
private Handler mHandler = new Handler(new MyHandlerCallback());
|
||||||
|
private Random mRnd = new Random();
|
||||||
|
private GameView mGameView;
|
||||||
|
private TextView mInfoView;
|
||||||
|
private Button mButtonNext;
|
||||||
|
|
||||||
/** Called when the activity is first created. */
|
/** Called when the activity is first created. */
|
||||||
@Override
|
@Override
|
||||||
@@ -32,22 +53,207 @@ public class GameActivity extends Activity {
|
|||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* IMPORTANT TIP: all resource IDs from this library must be
|
* IMPORTANT: all resource IDs from this library will eventually be merged
|
||||||
* different from the projects that will include it. E.g.
|
* with the resources from the main project that will use the library.
|
||||||
* if my layout were named "main.xml", I would have to use the ID
|
|
||||||
* R.layout.main; however there is already a *different*
|
|
||||||
* ID with the same name in the main project and when the library
|
|
||||||
* gets merged in the project the wrong ID would end up being used.
|
|
||||||
*
|
*
|
||||||
* To avoid such potential conflicts, it's probably a good idea
|
* If the main project and the libraries define the same resource IDs,
|
||||||
* to add a prefix to the library resource names.
|
* the application project will always have priority and override library resources
|
||||||
|
* and IDs defined in multiple libraries are resolved based on the libraries priority
|
||||||
|
* defined in the main project.
|
||||||
|
*
|
||||||
|
* An intentional consequence is that the main project can override some resources
|
||||||
|
* from the library.
|
||||||
|
* (TODO insert example).
|
||||||
|
*
|
||||||
|
* To avoid potential conflicts, it is suggested to add a prefix to the
|
||||||
|
* library resource names.
|
||||||
*/
|
*/
|
||||||
setContentView(R.layout.lib_game);
|
setContentView(R.layout.lib_game);
|
||||||
|
|
||||||
mTurnIsHuman = getIntent().getBooleanExtra(
|
mGameView = (GameView) findViewById(R.id.game_view);
|
||||||
EXTRA_START_WITH_HUMAN, true);
|
mInfoView = (TextView) findViewById(R.id.info_turn);
|
||||||
|
mButtonNext = (Button) findViewById(R.id.next_turn);
|
||||||
|
|
||||||
|
mGameView.setFocusable(true);
|
||||||
|
mGameView.setFocusableInTouchMode(true);
|
||||||
|
mGameView.setCellListener(new MyCellListener());
|
||||||
|
|
||||||
|
mButtonNext.setOnClickListener(new MyButtonListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
State player = mGameView.getCurrentPlayer();
|
||||||
|
if (player == State.UNKNOWN) {
|
||||||
|
player = State.fromInt(getIntent().getIntExtra(EXTRA_START_PLAYER, 1));
|
||||||
|
if (!checkGameFinished(player)) {
|
||||||
|
selectTurn(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (player == State.PLAYER2) {
|
||||||
|
mHandler.sendEmptyMessageDelayed(MSG_COMPUTER_TURN, COMPUTER_DELAY_MS);
|
||||||
|
}
|
||||||
|
if (player == State.WIN) {
|
||||||
|
setWinState(mGameView.getWinner());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private State selectTurn(State player) {
|
||||||
|
mGameView.setCurrentPlayer(player);
|
||||||
|
mButtonNext.setEnabled(false);
|
||||||
|
|
||||||
|
if (player == State.PLAYER1) {
|
||||||
|
mInfoView.setText("Player 1's turn -- that's you!");
|
||||||
|
mGameView.setEnabled(true);
|
||||||
|
|
||||||
|
} else if (player == State.PLAYER2) {
|
||||||
|
mInfoView.setText("Player 2's turn (that's the computer)");
|
||||||
|
mGameView.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyCellListener implements ICellListener {
|
||||||
|
public void onCellSelected() {
|
||||||
|
if (mGameView.getCurrentPlayer() == State.PLAYER1) {
|
||||||
|
int cell = mGameView.getSelection();
|
||||||
|
mButtonNext.setEnabled(cell >= 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyButtonListener implements OnClickListener {
|
||||||
|
|
||||||
|
public void onClick(View v) {
|
||||||
|
State player = mGameView.getCurrentPlayer();
|
||||||
|
|
||||||
|
if (player == State.WIN) {
|
||||||
|
GameActivity.this.finish();
|
||||||
|
|
||||||
|
} else if (player == State.PLAYER1) {
|
||||||
|
int cell = mGameView.getSelection();
|
||||||
|
if (cell >= 0) {
|
||||||
|
mGameView.stopBlink();
|
||||||
|
mGameView.setCell(cell, player);
|
||||||
|
finishTurn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyHandlerCallback implements Callback {
|
||||||
|
public boolean handleMessage(Message msg) {
|
||||||
|
if (msg.what == MSG_COMPUTER_TURN) {
|
||||||
|
|
||||||
|
// Pick a non-used cell at random. That's about all the AI you need for this game.
|
||||||
|
State[] data = mGameView.getData();
|
||||||
|
int used = 0;
|
||||||
|
while (used != 0x1F) {
|
||||||
|
int index = mRnd.nextInt(9);
|
||||||
|
if (((used >> index) & 1) == 0) {
|
||||||
|
used |= 1 << index;
|
||||||
|
if (data[index] == State.EMPTY) {
|
||||||
|
mGameView.setCell(index, mGameView.getCurrentPlayer());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishTurn();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private State getOtherPlayer(State player) {
|
||||||
|
return player == State.PLAYER1 ? State.PLAYER2 : State.PLAYER1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishTurn() {
|
||||||
|
State player = mGameView.getCurrentPlayer();
|
||||||
|
if (!checkGameFinished(player)) {
|
||||||
|
player = selectTurn(getOtherPlayer(player));
|
||||||
|
if (player == State.PLAYER2) {
|
||||||
|
mHandler.sendEmptyMessageDelayed(MSG_COMPUTER_TURN, COMPUTER_DELAY_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkGameFinished(State player) {
|
||||||
|
State[] data = mGameView.getData();
|
||||||
|
boolean full = true;
|
||||||
|
|
||||||
|
int col = -1;
|
||||||
|
int row = -1;
|
||||||
|
int diag = -1;
|
||||||
|
|
||||||
|
// check rows
|
||||||
|
for (int j = 0, k = 0; j < 3; j++, k += 3) {
|
||||||
|
if (data[k] != State.EMPTY && data[k] == data[k+1] && data[k] == data[k+2]) {
|
||||||
|
row = j;
|
||||||
|
}
|
||||||
|
if (full && (data[k] == State.EMPTY ||
|
||||||
|
data[k+1] == State.EMPTY ||
|
||||||
|
data[k+2] == State.EMPTY)) {
|
||||||
|
full = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check columns
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (data[i] != State.EMPTY && data[i] == data[i+3] && data[i] == data[i+6]) {
|
||||||
|
col = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check diagonals
|
||||||
|
if (data[0] != State.EMPTY && data[0] == data[1+3] && data[0] == data[2+6]) {
|
||||||
|
diag = 0;
|
||||||
|
} else if (data[2] != State.EMPTY && data[2] == data[1+3] && data[2] == data[0+6]) {
|
||||||
|
diag = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (col != -1 || row != -1 || diag != -1) {
|
||||||
|
setFinished(player, col, row, diag);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we get here, there's no winner but the board is full.
|
||||||
|
if (full) {
|
||||||
|
setFinished(State.EMPTY, -1, -1, -1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFinished(State player, int col, int row, int diagonal) {
|
||||||
|
|
||||||
|
mGameView.setCurrentPlayer(State.WIN);
|
||||||
|
mGameView.setWinner(player);
|
||||||
|
mGameView.setEnabled(false);
|
||||||
|
mGameView.setFinished(col, row, diagonal);
|
||||||
|
|
||||||
|
setWinState(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setWinState(State player) {
|
||||||
|
mButtonNext.setEnabled(true);
|
||||||
|
mButtonNext.setText("Back");
|
||||||
|
|
||||||
|
String text;
|
||||||
|
|
||||||
|
if (player == State.EMPTY) {
|
||||||
|
text = "This is a tie! No one wins!";
|
||||||
|
} else if (player == State.PLAYER1) {
|
||||||
|
text = "Player 1 (you) wins!";
|
||||||
|
} else {
|
||||||
|
text = "Player 2 (computer) wins!";
|
||||||
|
}
|
||||||
|
mInfoView.setText(text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package com.example.tictactoe.library;
|
package com.example.tictactoe.library;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.util.Random;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@@ -25,10 +25,14 @@ import android.graphics.BitmapFactory;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.Bitmap.Config;
|
||||||
import android.graphics.BitmapFactory.Options;
|
import android.graphics.BitmapFactory.Options;
|
||||||
|
import android.graphics.Paint.Style;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
import android.os.Parcelable;
|
||||||
import android.os.Handler.Callback;
|
import android.os.Handler.Callback;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -39,41 +43,166 @@ import android.view.View;
|
|||||||
|
|
||||||
public class GameView extends View {
|
public class GameView extends View {
|
||||||
|
|
||||||
private final static String TAG = "GameView";
|
private static final String TAG = "GameView";
|
||||||
|
|
||||||
private static final int MSG_FRAME = 1;
|
|
||||||
|
|
||||||
public static final long FPS_MS = 1000/2;
|
public static final long FPS_MS = 1000/2;
|
||||||
|
|
||||||
public static final int EMPTY = 0;
|
public enum State {
|
||||||
public static final int CROSS = 1;
|
UNKNOWN(-3),
|
||||||
public static final int CIRCLE = 2;
|
WIN(-2),
|
||||||
|
EMPTY(0),
|
||||||
|
PLAYER1(1),
|
||||||
|
PLAYER2(2);
|
||||||
|
|
||||||
|
private int mValue;
|
||||||
|
|
||||||
/** Contains one of {@link #EMPTY}, {@link #CROSS} or {@link #CIRCLE} */
|
private State(int value) {
|
||||||
private final int[] mData = new int[9];
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
private final Rect mBgRect = new Rect();
|
public int getValue() {
|
||||||
private final Rect mTempDst = new Rect();
|
return mValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static State fromInt(int i) {
|
||||||
|
for (State s : values()) {
|
||||||
|
if (s.getValue() == i) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int MARGIN = 4;
|
||||||
|
private static final int MSG_BLINK = 1;
|
||||||
|
|
||||||
|
private final Handler mHandler = new Handler(new MyHandler());
|
||||||
|
|
||||||
|
private final Rect mSrcRect = new Rect();
|
||||||
|
private final Rect mDstRect = new Rect();
|
||||||
|
|
||||||
private int mSxy;
|
private int mSxy;
|
||||||
private int mOffetX;
|
private int mOffetX;
|
||||||
private int mOffetY;
|
private int mOffetY;
|
||||||
|
private Paint mWinPaint;
|
||||||
private Paint mLinePaint;
|
private Paint mLinePaint;
|
||||||
|
private Paint mBmpPaint;
|
||||||
|
private Bitmap mBmpPlayer1;
|
||||||
|
private Bitmap mBmpPlayer2;
|
||||||
private Drawable mDrawableBg;
|
private Drawable mDrawableBg;
|
||||||
|
|
||||||
|
private ICellListener mCellListener;
|
||||||
|
|
||||||
|
/** Contains one of {@link State#EMPTY}, {@link State#PLAYER1} or {@link State#PLAYER2}. */
|
||||||
|
private final State[] mData = new State[9];
|
||||||
|
|
||||||
|
private int mSelectedCell = -1;
|
||||||
|
private State mSelectedValue = State.EMPTY;
|
||||||
|
private State mCurrentPlayer = State.UNKNOWN;
|
||||||
|
private State mWinner = State.EMPTY;
|
||||||
|
|
||||||
|
private int mWinCol = -1;
|
||||||
|
private int mWinRow = -1;
|
||||||
|
private int mWinDiag = -1;
|
||||||
|
|
||||||
|
private boolean mBlinkDisplayOff;
|
||||||
|
private final Rect mBlinkRect = new Rect();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public interface ICellListener {
|
||||||
|
abstract void onCellSelected();
|
||||||
|
}
|
||||||
|
|
||||||
public GameView(Context context, AttributeSet attrs) {
|
public GameView(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
requestFocus();
|
requestFocus();
|
||||||
|
|
||||||
mDrawableBg = getResources().getDrawable(R.drawable.lib_bg);
|
mDrawableBg = getResources().getDrawable(R.drawable.lib_bg);
|
||||||
|
setBackgroundDrawable(mDrawableBg);
|
||||||
|
|
||||||
|
mBmpPlayer1 = getResBitmap(R.drawable.lib_cross);
|
||||||
|
mBmpPlayer2 = getResBitmap(R.drawable.lib_circle);
|
||||||
|
|
||||||
|
if (mBmpPlayer1 != null) {
|
||||||
|
mSrcRect.set(0, 0, mBmpPlayer1.getWidth() -1, mBmpPlayer1.getHeight() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
mBmpPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
mLinePaint = new Paint();
|
mLinePaint = new Paint();
|
||||||
mLinePaint.setColor(0xFFFFFFFF);
|
mLinePaint.setColor(0xFFFFFFFF);
|
||||||
mLinePaint.setStrokeWidth(5);
|
mLinePaint.setStrokeWidth(5);
|
||||||
|
mLinePaint.setStyle(Style.STROKE);
|
||||||
|
|
||||||
|
mWinPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
mWinPaint.setColor(0xFFFF0000);
|
||||||
|
mWinPaint.setStrokeWidth(10);
|
||||||
|
mWinPaint.setStyle(Style.STROKE);
|
||||||
|
|
||||||
|
for (int i = 0; i < mData.length; i++) {
|
||||||
|
mData[i] = State.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isInEditMode()) {
|
||||||
|
// In edit mode (e.g. in the Eclipse ADT graphical layout editor)
|
||||||
|
// we'll use some random data to display the state.
|
||||||
|
Random rnd = new Random();
|
||||||
|
for (int i = 0; i < mData.length; i++) {
|
||||||
|
mData[i] = State.fromInt(rnd.nextInt(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public State[] getData() {
|
||||||
|
return mData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCell(int cellIndex, State value) {
|
||||||
|
mData[cellIndex] = value;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCellListener(ICellListener cellListener) {
|
||||||
|
mCellListener = cellListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelection() {
|
||||||
|
if (mSelectedValue == mCurrentPlayer) {
|
||||||
|
return mSelectedCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public State getCurrentPlayer() {
|
||||||
|
return mCurrentPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentPlayer(State player) {
|
||||||
|
mCurrentPlayer = player;
|
||||||
|
mSelectedCell = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public State getWinner() {
|
||||||
|
return mWinner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWinner(State winner) {
|
||||||
|
mWinner = winner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets winning mark on specified column or row (0..2) or diagonal (0..1). */
|
||||||
|
public void setFinished(int col, int row, int diagonal) {
|
||||||
|
mWinCol = col;
|
||||||
|
mWinRow = row;
|
||||||
|
mWinDiag = diagonal;
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDraw(Canvas canvas) {
|
protected void onDraw(Canvas canvas) {
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
@@ -83,24 +212,69 @@ public class GameView extends View {
|
|||||||
int x7 = mOffetX;
|
int x7 = mOffetX;
|
||||||
int y7 = mOffetY;
|
int y7 = mOffetY;
|
||||||
|
|
||||||
mDrawableBg.draw(canvas);
|
|
||||||
|
|
||||||
for (int i = 0, k = sxy; i < 2; i++, k += sxy) {
|
for (int i = 0, k = sxy; i < 2; i++, k += sxy) {
|
||||||
canvas.drawLine(x7 , y7 + k, x7 + s3, y7 + k , mLinePaint);
|
canvas.drawLine(x7 , y7 + k, x7 + s3 - 1, y7 + k , mLinePaint);
|
||||||
canvas.drawLine(x7 + k, y7 , x7 + k , y7 + s3, mLinePaint);
|
canvas.drawLine(x7 + k, y7 , x7 + k , y7 + s3 - 1, mLinePaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int j = 0, k = 0, y = y7; j < 3; j++, y += sxy) {
|
for (int j = 0, k = 0, y = y7; j < 3; j++, y += sxy) {
|
||||||
for (int i = 0, x = x7; i < 3; i++, x += sxy) {
|
for (int i = 0, x = x7; i < 3; i++, k++, x += sxy) {
|
||||||
|
mDstRect.offsetTo(MARGIN+x, MARGIN+y);
|
||||||
|
|
||||||
|
State v;
|
||||||
|
if (mSelectedCell == k) {
|
||||||
|
if (mBlinkDisplayOff) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
v = mSelectedValue;
|
||||||
|
} else {
|
||||||
|
v = mData[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(v) {
|
||||||
|
case PLAYER1:
|
||||||
|
if (mBmpPlayer1 != null) {
|
||||||
|
canvas.drawBitmap(mBmpPlayer1, mSrcRect, mDstRect, mBmpPaint);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PLAYER2:
|
||||||
|
if (mBmpPlayer2 != null) {
|
||||||
|
canvas.drawBitmap(mBmpPlayer2, mSrcRect, mDstRect, mBmpPaint);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mWinRow >= 0) {
|
||||||
|
int y = y7 + mWinRow * sxy + sxy / 2;
|
||||||
|
canvas.drawLine(x7 + MARGIN, y, x7 + s3 - 1 - MARGIN, y, mWinPaint);
|
||||||
|
|
||||||
|
} else if (mWinCol >= 0) {
|
||||||
|
int x = x7 + mWinCol * sxy + sxy / 2;
|
||||||
|
canvas.drawLine(x, y7 + MARGIN, x, y7 + s3 - 1 - MARGIN, mWinPaint);
|
||||||
|
|
||||||
|
} else if (mWinDiag == 0) {
|
||||||
|
// diagonal 0 is from (0,0) to (2,2)
|
||||||
|
|
||||||
|
canvas.drawLine(x7 + MARGIN, y7 + MARGIN,
|
||||||
|
x7 + s3 - 1 - MARGIN, y7 + s3 - 1 - MARGIN, mWinPaint);
|
||||||
|
|
||||||
|
} else if (mWinDiag == 1) {
|
||||||
|
// diagonal 1 is from (0,2) to (2,0)
|
||||||
|
|
||||||
|
canvas.drawLine(x7 + MARGIN, y7 + s3 - 1 - MARGIN,
|
||||||
|
x7 + s3 - 1 - MARGIN, y7 + MARGIN, mWinPaint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
// Keep the view squared
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
int w = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int h = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
int d = w == 0 ? h : h == 0 ? w : w < h ? w : h;
|
||||||
|
setMeasuredDimension(d, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -108,8 +282,8 @@ public class GameView extends View {
|
|||||||
super.onSizeChanged(w, h, oldw, oldh);
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
Log.d(TAG, String.format("onSizeChanged: %dx%d", w, h));
|
Log.d(TAG, String.format("onSizeChanged: %dx%d", w, h));
|
||||||
|
|
||||||
int sx = w / 3;
|
int sx = (w - 2 * MARGIN) / 3;
|
||||||
int sy = h / 3;
|
int sy = (h - 2 * MARGIN) / 3;
|
||||||
|
|
||||||
int size = sx < sy ? sx : sy;
|
int size = sx < sy ? sx : sy;
|
||||||
|
|
||||||
@@ -117,8 +291,7 @@ public class GameView extends View {
|
|||||||
mOffetX = (w - 3 * size) / 2;
|
mOffetX = (w - 3 * size) / 2;
|
||||||
mOffetY = (h - 3 * size) / 2;
|
mOffetY = (h - 3 * size) / 2;
|
||||||
|
|
||||||
mDrawableBg.setBounds(mOffetX, mOffetY,
|
mDstRect.set(MARGIN, MARGIN, size - MARGIN, size - MARGIN);
|
||||||
mOffetX + 3 * size, mOffetY + 3 * size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -128,23 +301,170 @@ public class GameView extends View {
|
|||||||
if (action == MotionEvent.ACTION_DOWN) {
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
} else if (action == MotionEvent.ACTION_UP) {
|
||||||
float x = event.getX();
|
int x = (int) event.getX();
|
||||||
float y = event.getY();
|
int y = (int) event.getY();
|
||||||
|
|
||||||
|
int sxy = mSxy;
|
||||||
|
x = (x - MARGIN) / sxy;
|
||||||
|
y = (y - MARGIN) / sxy;
|
||||||
|
|
||||||
|
if (isEnabled() && x >= 0 && x < 3 && y >= 0 & y < 3) {
|
||||||
|
int cell = x + 3 * y;
|
||||||
|
|
||||||
|
State state = cell == mSelectedCell ? mSelectedValue : mData[cell];
|
||||||
|
state = state == State.EMPTY ? mCurrentPlayer : State.EMPTY;
|
||||||
|
|
||||||
|
stopBlink();
|
||||||
|
|
||||||
|
mSelectedCell = cell;
|
||||||
|
mSelectedValue = state;
|
||||||
|
mBlinkDisplayOff = false;
|
||||||
|
mBlinkRect.set(MARGIN + x * sxy, MARGIN + y * sxy,
|
||||||
|
MARGIN + (x + 1) * sxy, MARGIN + (y + 1) * sxy);
|
||||||
|
|
||||||
|
if (state != State.EMPTY) {
|
||||||
|
// Start the blinker
|
||||||
|
mHandler.sendEmptyMessageDelayed(MSG_BLINK, FPS_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCellListener != null) {
|
||||||
|
mCellListener.onCellSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap getResBitmap(int bmpResId) {
|
public void stopBlink() {
|
||||||
|
boolean hadSelection = mSelectedCell != -1 && mSelectedValue != State.EMPTY;
|
||||||
|
mSelectedCell = -1;
|
||||||
|
mSelectedValue = State.EMPTY;
|
||||||
|
if (!mBlinkRect.isEmpty()) {
|
||||||
|
invalidate(mBlinkRect);
|
||||||
|
}
|
||||||
|
mBlinkDisplayOff = false;
|
||||||
|
mBlinkRect.setEmpty();
|
||||||
|
mHandler.removeMessages(MSG_BLINK);
|
||||||
|
if (hadSelection && mCellListener != null) {
|
||||||
|
mCellListener.onCellSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Parcelable onSaveInstanceState() {
|
||||||
|
Bundle b = new Bundle();
|
||||||
|
|
||||||
|
Parcelable s = super.onSaveInstanceState();
|
||||||
|
b.putParcelable("gv_super_state", s);
|
||||||
|
|
||||||
|
b.putBoolean("gv_en", isEnabled());
|
||||||
|
|
||||||
|
int[] data = new int[mData.length];
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
data[i] = mData[i].getValue();
|
||||||
|
}
|
||||||
|
b.putIntArray("gv_data", data);
|
||||||
|
|
||||||
|
b.putInt("gv_sel_cell", mSelectedCell);
|
||||||
|
b.putInt("gv_sel_val", mSelectedValue.getValue());
|
||||||
|
b.putInt("gv_curr_play", mCurrentPlayer.getValue());
|
||||||
|
b.putInt("gv_winner", mWinner.getValue());
|
||||||
|
|
||||||
|
b.putInt("gv_win_col", mWinCol);
|
||||||
|
b.putInt("gv_win_row", mWinRow);
|
||||||
|
b.putInt("gv_win_diag", mWinDiag);
|
||||||
|
|
||||||
|
b.putBoolean("gv_blink_off", mBlinkDisplayOff);
|
||||||
|
b.putParcelable("gv_blink_rect", mBlinkRect);
|
||||||
|
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRestoreInstanceState(Parcelable state) {
|
||||||
|
|
||||||
|
if (!(state instanceof Bundle)) {
|
||||||
|
// Not supposed to happen.
|
||||||
|
super.onRestoreInstanceState(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle b = (Bundle) state;
|
||||||
|
Parcelable superState = b.getParcelable("gv_super_state");
|
||||||
|
|
||||||
|
setEnabled(b.getBoolean("gv_en", true));
|
||||||
|
|
||||||
|
int[] data = b.getIntArray("gv_data");
|
||||||
|
if (data != null && data.length == mData.length) {
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
mData[i] = State.fromInt(data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mSelectedCell = b.getInt("gv_sel_cell", -1);
|
||||||
|
mSelectedValue = State.fromInt(b.getInt("gv_sel_val", State.EMPTY.getValue()));
|
||||||
|
mCurrentPlayer = State.fromInt(b.getInt("gv_curr_play", State.EMPTY.getValue()));
|
||||||
|
mWinner = State.fromInt(b.getInt("gv_winner", State.EMPTY.getValue()));
|
||||||
|
|
||||||
|
mWinCol = b.getInt("gv_win_col", -1);
|
||||||
|
mWinRow = b.getInt("gv_win_row", -1);
|
||||||
|
mWinDiag = b.getInt("gv_win_diag", -1);
|
||||||
|
|
||||||
|
mBlinkDisplayOff = b.getBoolean("gv_blink_off", false);
|
||||||
|
Rect r = b.getParcelable("gv_blink_rect");
|
||||||
|
if (r != null) {
|
||||||
|
mBlinkRect.set(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// let the blink handler decide if it should blink or not
|
||||||
|
mHandler.sendEmptyMessage(MSG_BLINK);
|
||||||
|
|
||||||
|
super.onRestoreInstanceState(superState);
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----
|
||||||
|
|
||||||
|
private class MyHandler implements Callback {
|
||||||
|
public boolean handleMessage(Message msg) {
|
||||||
|
if (msg.what == MSG_BLINK) {
|
||||||
|
if (mSelectedCell >= 0 && mSelectedValue != State.EMPTY && mBlinkRect.top != 0) {
|
||||||
|
mBlinkDisplayOff = !mBlinkDisplayOff;
|
||||||
|
invalidate(mBlinkRect);
|
||||||
|
|
||||||
|
if (!mHandler.hasMessages(MSG_BLINK)) {
|
||||||
|
mHandler.sendEmptyMessageDelayed(MSG_BLINK, FPS_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap getResBitmap(int bmpResId) {
|
||||||
Options opts = new Options();
|
Options opts = new Options();
|
||||||
opts.inDither = false;
|
opts.inDither = false;
|
||||||
|
|
||||||
Resources res = getResources();
|
Resources res = getResources();
|
||||||
Bitmap bmp = BitmapFactory.decodeResource(res, bmpResId, opts);
|
Bitmap bmp = BitmapFactory.decodeResource(res, bmpResId, opts);
|
||||||
|
|
||||||
|
if (bmp == null && isInEditMode()) {
|
||||||
|
// BitmapFactory.decodeResource doesn't work from the rendering
|
||||||
|
// library in Eclipse's Graphical Layout Editor. Use this workaround instead.
|
||||||
|
|
||||||
|
Drawable d = res.getDrawable(bmpResId);
|
||||||
|
int w = d.getIntrinsicWidth();
|
||||||
|
int h = d.getIntrinsicHeight();
|
||||||
|
bmp = Bitmap.createBitmap(w, h, Config.ARGB_8888);
|
||||||
|
Canvas c = new Canvas(bmp);
|
||||||
|
d.setBounds(0, 0, w - 1, h - 1);
|
||||||
|
d.draw(c);
|
||||||
|
}
|
||||||
|
|
||||||
return bmp;
|
return bmp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<!-- This is defined in TicTacToeLib. Right now we need to manually
|
<!-- This is defined in TicTacToeLib. Right now we need to manually
|
||||||
copy it here. Eventually it should get merged automatically. -->
|
copy it here. Eventually it should get merged automatically. -->
|
||||||
<activity android:name=".library.GameActivity" />
|
<activity android:name="com.example.tictactoe.library.GameActivity" />
|
||||||
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
48
samples/TicTacToeMain/README.txt
Executable file
48
samples/TicTacToeMain/README.txt
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
Sample: TicTacToeLib and TicTacToeMain.
|
||||||
|
|
||||||
|
--------
|
||||||
|
Summary:
|
||||||
|
--------
|
||||||
|
|
||||||
|
These two projects work together. They demonstrate how to use the ability to
|
||||||
|
split an APK into multiple projects.
|
||||||
|
|
||||||
|
Build is supported both via Ant (command-line tools) or via ADT (the Android
|
||||||
|
plugin for Eclipse).
|
||||||
|
|
||||||
|
--------
|
||||||
|
Details:
|
||||||
|
--------
|
||||||
|
|
||||||
|
TicTacToeMain is the main project. It defines a main activity that is first
|
||||||
|
displayed to the user. When one of the start buttons is selected, an
|
||||||
|
activity defined in TicTacToeLib is started.
|
||||||
|
|
||||||
|
To define that TicTacToeMain uses TicTacToeLib as a "project library", the
|
||||||
|
file TicTacToeMain/default.properties contains the special line:
|
||||||
|
android.library.reference.1=../TicTacToeLib/
|
||||||
|
|
||||||
|
|
||||||
|
TicTacToeLib is the "project library". It can contain both source code (.java)
|
||||||
|
and Android resources (anything under /res) that will be merged in the final
|
||||||
|
APK. To define this is a library, the file TicTacToeLib/default.project
|
||||||
|
contains the special line:
|
||||||
|
android.library=true
|
||||||
|
|
||||||
|
|
||||||
|
One important thing to realize is that the library is not a separately-compiled
|
||||||
|
JAR file: the source and resources from the library are _actually_ merged in
|
||||||
|
the main project and the result is used to generate the APK. This means that
|
||||||
|
the main project can either use or redefine behavior from the libraries.
|
||||||
|
|
||||||
|
|
||||||
|
To use the main vs livrary project:
|
||||||
|
- In ADT, just open import both projects and launch the main project.
|
||||||
|
- In Ant, use 'android update project' to create the build files and set the SDK location,
|
||||||
|
and then run 'ant debug' on the main project.
|
||||||
|
|
||||||
|
|
||||||
|
For more details on the purpose of this feature, its limitations and detailed usage,
|
||||||
|
please read the SDK guide at
|
||||||
|
<STOPSHIP insert URL here to library-project on developers.android.com>
|
||||||
|
|
||||||
@@ -24,5 +24,6 @@
|
|||||||
# project structure.
|
# project structure.
|
||||||
|
|
||||||
android.library.reference.1=../TicTacToeLib/
|
android.library.reference.1=../TicTacToeLib/
|
||||||
# Project target.
|
# Project target. This requires the tools from SDK Froyo (API 8) to be used.
|
||||||
|
# STOPSHIP change to target=8 once Froyo API is set to 8.
|
||||||
target=android-Froyo
|
target=android-Froyo
|
||||||
|
|||||||
@@ -21,6 +21,5 @@
|
|||||||
<string name="welcome"><b>Welcome to the Tic-Tac-Toe Sample!</b></string>
|
<string name="welcome"><b>Welcome to the Tic-Tac-Toe Sample!</b></string>
|
||||||
<string name="explain1">This sample code demonstrates how to split an application in multiple projects by using the \'project library\' available in the Froyo SDK Tools.</string>
|
<string name="explain1">This sample code demonstrates how to split an application in multiple projects by using the \'project library\' available in the Froyo SDK Tools.</string>
|
||||||
<string name="explain2">This activity is defined in one project. The second activity, launched by one of the buttons below, is located in another project which is a \"library\" to the main one and merged in the same APK.</string>
|
<string name="explain2">This activity is defined in one project. The second activity, launched by one of the buttons below, is located in another project which is a \"library\" to the main one and merged in the same APK.</string>
|
||||||
<string name="hello">Hello World, MainActivity!</string>
|
|
||||||
<string name="app_name">Tic-Tac-Toe Sample</string>
|
<string name="app_name">Tic-Tac-Toe Sample</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
package com.example.tictactoe;
|
package com.example.tictactoe;
|
||||||
|
|
||||||
import com.example.tictactoe.library.GameActivity;
|
import com.example.tictactoe.library.GameActivity;
|
||||||
|
import com.example.tictactoe.library.GameView.State;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -47,7 +48,8 @@ public class MainActivity extends Activity {
|
|||||||
|
|
||||||
private void startGame(boolean startWithHuman) {
|
private void startGame(boolean startWithHuman) {
|
||||||
Intent i = new Intent(this, GameActivity.class);
|
Intent i = new Intent(this, GameActivity.class);
|
||||||
i.putExtra(GameActivity.EXTRA_START_WITH_HUMAN, startWithHuman);
|
i.putExtra(GameActivity.EXTRA_START_PLAYER,
|
||||||
|
startWithHuman ? State.PLAYER1.getValue() : State.PLAYER2.getValue());
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user