검색결과 리스트
# 전체보기에 해당되는 글 19건
- 2014.03.04 Wi-Fi P2P를 통해 서비스 제공하기
- 2014.03.04 Wi-Fi P2P 연결하기
- 2014.02.20 Network Service Discovery(NSD) 사용하기
- 2014.02.13 문서를 수동 설정하여 프린터로 인쇄하기
- 2014.02.13 HTML 문서를 프린터로 인쇄하기
- 2014.02.12 간단한 사진을 프린터로 인쇄하기
- 2014.02.10 카메라 제어
- 2014.02.07 간단히 동영상 찍기
- 2014.01.29 간단히 사진 찍기
- 2014.01.28 오디오 제어
글
Wi-Fi P2P를 통해 서비스 제공하기
- Wi-Fi P2P를 통해서 NSD와 같이 서비스를 검색할 수 있도록하는 방법이다.
- 이전 "Wi-Fi P2P 연결하기"와 동일한 퍼미션을 사용한다.
- 서비스를 등록하기 위해서 아래와 같이 addLocalService()
를 호출한다. 아래에서 record는 클라이언트에 제공하는 데이터이다.
private void startRegistration() {
// Create a string map containing information about your service.
Maprecord = new HashMap ();
record.put("listenport", String.valueOf(SERVER_PORT));
record.put("buddyname", "John Doe" + (int) (Math.random() * 1000));
record.put("available", "visible");
// Service information. Pass it an instance name, service type
// _protocol._transportlayer , and the map containing
// information other devices will want once they connect to this one.
WifiP2pDnsSdServiceInfo serviceInfo =
WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record);
// Add the local service, sending the service info, network channel,
// and listener that will be used to indicate success or failure of
// the request.
mManager.addLocalService(channel, serviceInfo, new ActionListener() {
@Override
public void onSuccess() {
// Command successful! Code isn't necessarily needed here,
// Unless you want to update the UI or add logging statements.
}
@Override
public void onFailure(int arg0) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
}
});
}
- 주변에서 제공되는 서비스 검색을 요청할 때, 아래 2개의 Listener를 등록해서 Broadcast되는 서비스 정보를 저장한다.
final HashMap<String, String> buddies = new HashMap<String, String>();
...
private void discoverService() {
DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() {
@Override
/* Callback includes:
* fullDomain: full domain name: e.g "printer._ipp._tcp.local."
* record: TXT record dta as a map of key/value pairs.
* device: The device running the advertised service.
*/
public void onDnsSdTxtRecordAvailable(
String fullDomain, Maprecord, WifiP2pDevice device) {
Log.d(TAG, "DnsSdTxtRecord available -" + record.toString());
buddies.put(device.deviceAddress, record.get("buddyname"));
}
};
...
DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() {
@Override
public void onDnsSdServiceAvailable(String instanceName, String registrationType,
WifiP2pDevice resourceType) {
// Update the device name with the human-friendly version from
// the DnsTxtRecord, assuming one arrived.
resourceType.deviceName = buddies
.containsKey(resourceType.deviceAddress) ? buddies
.get(resourceType.deviceAddress) : resourceType.deviceName;
// Add to the custom adapter defined specifically for showing
// wifi devices.
WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager()
.findFragmentById(R.id.frag_peerlist);
WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment
.getListAdapter());
adapter.add(resourceType);
adapter.notifyDataSetChanged();
Log.d(TAG, "onBonjourServiceAvailable " + instanceName);
}
};
mManager.setDnsSdResponseListeners(channel, servListener, txtListener);
...
}
- 검색 요청은 아래 2개 메소드를 호출하면 된다.
serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
mManager.addServiceRequest(channel,
serviceRequest,
new ActionListener() {
@Override
public void onSuccess() {
// Success!
}
@Override
public void onFailure(int code) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
}
});
mManager.discoverServices(channel, new ActionListener() {
@Override
public void onSuccess() {
// Success!
}
@Override
public void onFailure(int code) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
if (code == WifiP2pManager.P2P_UNSUPPORTED) {
Log.d(TAG, "P2P isn't supported on this device.");
else if(...)
...
}
});
출처 : http://developer.android.com/
글
Wi-Fi P2P 연결하기
- Android 4.0(API Level 14) 부터 Wi-Fi P2P 연결 관련 API를 제공한다.
- 아래와 같은 퍼미션이 필요하다.
<uses-permission
android:required="true"
android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.INTERNET"/>
- WifiP2pManager
를 아래와 같이 초기화해준다. 생성된 WifiP2pManager.Channel
를 통해 Wi-Fi P2P Framework에 접속하게 된다.
@Override
Channel mChannel;
public void onCreate(Bundle savedInstanceState) {
....
mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
mChannel = mManager.initialize(this, getMainLooper(), null);
}
- BroadcastReceiver에 등록할 IntentFilter,이벤트를 처리할 onReceive(), 생명주기 관련 코드는 아래와 같이 작성한다.
private final IntentFilter intentFilter = new IntentFilter();
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Indicates a change in the Wi-Fi P2P status.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
// Indicates a change in the list of available peers.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
// Indicates the state of Wi-Fi P2P connectivity has changed.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
// Indicates this device's details have changed.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
...
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
// Determine if Wifi P2P mode is enabled or not, alert
// the Activity.
int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
activity.setIsWifiP2pEnabled(true);
} else {
activity.setIsWifiP2pEnabled(false);
}
} else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
// The peer list has changed! We should probably do something about
// that.
} else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
// Connection state changed! We should probably do something about
// that.
} else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager()
.findFragmentById(R.id.frag_list);
fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra(
WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));
}
}
/** register the BroadcastReceiver with the intent values to be matched */
@Override
public void onResume() {
super.onResume();
receiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this);
registerReceiver(receiver, intentFilter);
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(receiver);
}
- 주변에 있는 Peer들을 검색하기 위해서는 discoverPeers()
를 아래와 같이 호출한다. 이것은 검색 시작을 의미하며, 실제로 Peer들이 검색되었을 때는 위의 onReceive()에서 판단한다.
mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
// Code for when the discovery initiation is successful goes here.
// No services have actually been discovered yet, so this method
// can often be left blank. Code for peer discovery goes in the
// onReceive method, detailed below.
}
@Override
public void onFailure(int reasonCode) {
// Code for when the discovery initiation fails goes here.
// Alert the user that something went wrong.
}
});
- 실제 Peer들의 목록을 가져오기 위해서는 위 onReceive()의 WIFI_P2P_PEERS_CHANGED_ACTION에서 requestPeers()
를 호출해야 한다.
public void onReceive(Context context, Intent intent) {
...
else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
// Request available peers from the wifi p2p manager. This is an
// asynchronous call and the calling activity is notified with a
// callback on PeerListListener.onPeersAvailable()
if (mManager != null) {
mManager.requestPeers(mChannel, peerListListener);
}
Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
}...
}
private Listpeers = new ArrayList ();
...
private PeerListListener peerListListener = new PeerListListener() {
@Override
public void onPeersAvailable(WifiP2pDeviceList peerList) {
// Out with the old, in with the new.
peers.clear();
peers.addAll(peerList.getDeviceList());
// If an AdapterView is backed by this data, notify it
// of the change. For instance, if you have a ListView of available
// peers, trigger an update.
((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();
if (peers.size() == 0) {
Log.d(WiFiDirectActivity.TAG, "No devices found");
return;
}
}
}
- Peer에 접속하기 위해서 아래와 같이 connect()
를 호출한다.
@Override
public void connect() {
// Picking the first device found on the network.
WifiP2pDevice device = peers.get(0);
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = device.deviceAddress;
config.wps.setup = WpsInfo.PBC;
mManager.connect(mChannel, config, new ActionListener() {
@Override
public void onSuccess() {
// WiFiDirectBroadcastReceiver will notify us. Ignore for now.
}
@Override
public void onFailure(int reason) {
Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
Toast.LENGTH_SHORT).show();
}
});
}
- 접속이 되면 관련 정보를 얻기 위해 onReceive()에서 아래와 같이 처리한다.
...
} else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
if (mManager == null) {
return;
}
NetworkInfo networkInfo = (NetworkInfo) intent
.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
if (networkInfo.isConnected()) {
// We are connected with the other device, request connection
// info to find group owner IP
mManager.requestConnectionInfo(mChannel, connectionListener);
}
...
- 위의 WifiP2pManager.ConnectionInfoListener는 아래와 같다. 자신이 "group owner"이면 서버 역할을 해야 한다.
@Override
public void onConnectionInfoAvailable(final WifiP2pInfo info) {
// InetAddress from WifiP2pInfo struct.
InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress());
// After the group negotiation, we can determine the group owner.
if (info.groupFormed && info.isGroupOwner) {
// Do whatever tasks are specific to the group owner.
// One common case is creating a server thread and accepting
// incoming connections.
} else if (info.groupFormed) {
// The other device acts as the client. In this case,
// you'll want to create a client thread that connects to the group
// owner.
}
}
- 이후에 상대의 IP에 접속하여 통신을 하면 된다.
출처 : http://developer.android.com/
글
Network Service Discovery(NSD) 사용하기
- Android 4.1(API Level 16) 부터는 동일 로컬 네트워크 상에 있는 단말간의 접속을 쉽게 할 수 있도록 Network Service Discovery(NSD) API들을 제공한다.
- 로컬 네트워크에 NSD 서비스를 제공(서버 역할)하려면, 아래와 같이 NsdServiceInfo 객체를 생성해야 한다.
public void registerService(int port) {
// Create the NsdServiceInfo object, and populate it.
NsdServiceInfo serviceInfo = new NsdServiceInfo();
// The name is subject to change based on conflicts
// with other services advertised on the same network.
serviceInfo.setServiceName("NsdChat");
serviceInfo.setServiceType("_http._tcp.");
serviceInfo.setPort(port);
....
}
- "NsdChat"은 다른 단말들에서 보여질 서비스 이름이다. 로컬 네트워크 상에 유일해야 한다. 만약, 이름이 겹치게 되면 "NsdChat (1)"과 같이 이름이 자동으로 변경된다.
- "_http._tcp"은 제공될 서비스 타입이다. 문법은 "_<protocol>._<transportlayer>"이며, IANA의 서비스 이름/포트 목록 URL에서 찾아서 입력하면된다. (신규 등록 URL)
- port는 다른 App.과 충돌할 가능성이 있기 때문에, 하드 코딩해서 넣으면 안된다. 만약, TCP를 사용한다면 아래와 같이 사용 가능한 포트번호를 가져올 수 있다.
public void initializeServerSocket() {
// Initialize a server socket on the next available port.
mServerSocket = new ServerSocket(0);
// Store the chosen port.
mLocalPort = mServerSocket.getLocalPort();
...
}
- NSD를 등록할 때 필요한 Listener는 아래와 같다.
public void initializeRegistrationListener() {
mRegistrationListener = new NsdManager.RegistrationListener() {
@Override
public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
// Save the service name. Android may have changed it in order to
// resolve a conflict, so update the name you initially requested
// with the name Android actually used.
mServiceName = NsdServiceInfo.getServiceName();
}
@Override
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Registration failed! Put debugging code here to determine why.
}
@Override
public void onServiceUnregistered(NsdServiceInfo arg0) {
// Service has been unregistered. This only happens when you call
// NsdManager.unregisterService() and pass in this listener.
}
@Override
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Unregistration failed. Put debugging code here to determine why.
}
};
}
- 이제 아래와 같이 등록하면, 서비스 제공이 시작된다. Async하게 동작하므로 위 Listener의 onServiceRegisterd()가 호출될때가 등록이 완료된 때이다.
public void registerService(int port) {
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName("NsdChat");
serviceInfo.setServiceType("_http._tcp.");
serviceInfo.setPort(port);
mNsdManager = Context.getSystemService(Context.NSD_SERVICE);
mNsdManager.registerService(
serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
}
- 로컬 네트워크에 제공중인 NSD 서비스를 이용(클라이언트 역할)하려면, 아래와 같은 Listener를 등록하고 discoverServices() 를 호출해주면 된다. 서비스가 찾아졌을때에는 ServiceType이 맞는지, 자기 자신이 아닌지, ServiceName이 맞는지 확인해야 한다.
public void initializeDiscoveryListener() {
// Instantiate a new DiscoveryListener
mDiscoveryListener = new NsdManager.DiscoveryListener() {
// Called as soon as service discovery begins.
@Override
public void onDiscoveryStarted(String regType) {
Log.d(TAG, "Service discovery started");
}
@Override
public void onServiceFound(NsdServiceInfo service) {
// A service was found! Do something with it.
Log.d(TAG, "Service discovery success" + service);
if (!service.getServiceType().equals(SERVICE_TYPE)) {
// Service type is the string containing the protocol and
// transport layer for this service.
Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
} else if (service.getServiceName().equals(mServiceName)) {
// The name of the service tells the user what they'd be
// connecting to. It could be "Bob's Chat App".
Log.d(TAG, "Same machine: " + mServiceName);
} else if (service.getServiceName().contains("NsdChat")){
mNsdManager.resolveService(service, mResolveListener);
}
}
@Override
public void onServiceLost(NsdServiceInfo service) {
// When the network service is no longer available.
// Internal bookkeeping code goes here.
Log.e(TAG, "service lost" + service);
}
@Override
public void onDiscoveryStopped(String serviceType) {
Log.i(TAG, "Discovery stopped: " + serviceType);
}
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
mNsdManager.stopServiceDiscovery(this);
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
mNsdManager.stopServiceDiscovery(this);
}
};
}
mNsdManager.discoverServices(
SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
- 위 코드의 resolveService() 는 발견된 서비스에 접속을 하는 API이다. Listener는 아래와 같이 생성하여 등록한다.
public void initializeResolveListener() {
mResolveListener = new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Called when the resolve fails. Use the error code to debug.
Log.e(TAG, "Resolve failed" + errorCode);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
Log.e(TAG, "Resolve Succeeded. " + serviceInfo);
if (serviceInfo.getServiceName().equals(mServiceName)) {
Log.d(TAG, "Same IP.");
return;
}
mService = serviceInfo;
int port = mService.getPort();
InetAddress host = mService.getHost();
}
};
}
- NSD LifeCycle 관리는 아래와 같이 해준다.
//In your application's Activity
@Override
protected void onPause() {
if (mNsdHelper != null) {
mNsdHelper.tearDown();
}
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
if (mNsdHelper != null) {
mNsdHelper.registerService(mConnection.getLocalPort());
mNsdHelper.discoverServices();
}
}
@Override
protected void onDestroy() {
mNsdHelper.tearDown();
mConnection.tearDown();
super.onDestroy();
}
// NsdHelper's tearDown method
public void tearDown() {
mNsdManager.unregisterService(mRegistrationListener);
mNsdManager.stopServiceDiscovery(mDiscoveryListener);
}
출처 : http://developer.android.com/
글
문서를 수동 설정하여 프린터로 인쇄하기
- PrintManager
객체를 얻어서, 인쇄하기 위해서 아래와 같이 한다.
private void doPrint() {
// Get a PrintManager instance
PrintManager printManager = (PrintManager) getActivity()
.getSystemService(Context.PRINT_SERVICE);
// Set job name, which will be displayed in the print queue
String jobName = getActivity().getString(R.string.app_name) + " Document";
// Start a print job, passing in a PrintDocumentAdapter implementation
// to handle the generation of a print document
printManager.print(jobName, new MyPrintDocumentAdapter(getActivity()),
null); //
}
- 인쇄할 내용에 대한 정의는 PrintDocumentAdapter에서 하게 되며, 인쇄할 때의 다양한 옵션(가로/세로 모드 등)은 print()의 마지막 인자값(위 코드의 null)인 PrintAttributes
을 통해서 설정할 수 있다.
- PrintDocumentAdapter는 인쇄 Lifecycle에 해당하는 아래 4개의 콜백 메소드를 가지고 있다. 이 메소드들은 Main Thread에서 돌아가므로, 오래 걸리는 작업은 AsyncTask등을 이용해서 별도 Thread로 돌려야 한다.
onStart() : 인쇄 시작할때 불린다. (필수 아님)
onLayout() : 인쇄 결과물에 영향을 주는 옵션(페이지 크기, 인쇄 방향 등)을 사용자가 변경할 때 불린다. 변경된 옵션(페이지 크기 등)에 맞는 내용을 계산해서 Framework에 관련 내용(PrintDocumentInfo
, 페이지수 등의 정보를 담고 있음)을 설정한다.
onWrite() : 인쇄할 페이지 내용에 대한 랜더링이 필요할때 불린다. onLayout()이 불린 후, 1번 이상 불린다.
onFinish() : 인쇄가 끝났을때 불린다. (필수 아님)
- onLayout()은 LayoutResultCallback을 통해 3종류(설정 완료, 취소, 실패)의 결과를 반환할 수 있다. 아래와 같이 구현한다.
@Override
public void onLayout(PrintAttributes oldAttributes,
PrintAttributes newAttributes,
CancellationSignal cancellationSignal,
LayoutResultCallback callback,
Bundle metadata) {
// Create a new PdfDocument with the requested page attributes
mPdfDocument = new PrintedPdfDocument(getActivity(), newAttributes);
// Respond to cancellation request
if (cancellationSignal.isCancelled() ) {
callback.onLayoutCancelled();
return;
}
// Compute the expected number of printed pages
int pages = computePageCount(newAttributes);
if (pages > 0) {
// Return print information to print framework
PrintDocumentInfo info = new PrintDocumentInfo
.Builder("print_output.pdf")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.setPageCount(pages);
.build();
// Content layout reflow is complete
callback.onLayoutFinished(info, true);
} else {
// Otherwise report an error to the print framework
callback.onLayoutFailed("Page count calculation failed.");
}
}
- onLayoutFinished()
의 두번째 인자값(true)은 기존의 설정에서 변경된 것이 있는지를 나타내며, 필요없는 onWrite() 호출을 막을 수 있다. 성능 향상을 위해서는 기존 설정값을 저장/비교하여 이 인자값을 잘 보내주도록 한다.
- 위 코드의 computePageCount()는 아래와 같이 구현한다.
private int computePageCount(PrintAttributes printAttributes) {
int itemsPerPage = 4; // default item count for portrait mode
MediaSize pageSize = printAttributes.getMediaSize();
if (!pageSize.isPortrait()) {
// Six items per page in landscape orientation
itemsPerPage = 6;
}
// Determine number of print items
int printItemCount = getPrintItemCount();
return (int) Math.ceil(printItemCount / itemsPerPage);
}
- onWrite()는 WriteResultCallback을 통해 3종류(설정 완료, 취소, 실패)의 결과를 반환할 수 있다. 아래는 PrintedPdfDocument을 사용하여 PDF 파일을 출력하는 예제이다.
@Override
public void onWrite(final PageRange[] pageRanges,
final ParcelFileDescriptor destination,
final CancellationSignal cancellationSignal,
final WriteResultCallback callback) {
// Iterate over each page of the document,
// check if it's in the output range.
for (int i = 0; i < totalPages; i++) {
// Check to see if this page is in the output range.
if (containsPage(pageRanges, i)) {
// If so, add it to writtenPagesArray. writtenPagesArray.size()
// is used to compute the next output page index.
writtenPagesArray.append(writtenPagesArray.size(), i);
PdfDocument.Page page = mPdfDocument.startPage(i);
// check for cancellation
if (cancellationSignal.isCancelled()) {
callback.onWriteCancelled();
mPdfDocument.close();
mPdfDocument = null;
return;
}
// Draw page content for printing
drawPage(page);
// Rendering is complete, so page can be finalized.
mPdfDocument.finishPage(page);
}
}
// Write PDF document to file
try {
mPdfDocument.writeTo(new FileOutputStream(
destination.getFileDescriptor()));
} catch (IOException e) {
callback.onWriteFailed(e.toString());
return;
} finally {
mPdfDocument.close();
mPdfDocument = null;
}
PageRange[] writtenPages = computeWrittenPages();
// Signal the print framework the document is complete
callback.onWriteFinished(writtenPages);
...
}
- 위 코드의 drawPage()는 추가적으로 출력하고 싶은 내용을 그리는 메소드이며, 아래와 같이 구현한 수 있다. Page의 Canvas에 그릴때 종이 가장자리에 찍히지 않는 영역이 있다는 것을 고려해야 한다.
private void drawPage(PdfDocument.Page page) {
Canvas canvas = page.getCanvas();
// units are in points (1/72 of an inch)
int titleBaseLine = 72;
int leftMargin = 54;
Paint paint = new Paint();
paint.setColor(Color.BLACK);
paint.setTextSize(36);
canvas.drawText("Test Title", leftMargin, titleBaseLine, paint);
paint.setTextSize(11);
canvas.drawText("Test paragraph", leftMargin, titleBaseLine + 25, paint);
paint.setColor(Color.BLUE);
canvas.drawRect(100, 100, 172, 172, paint);
}
출처 : http://developer.android.com/
글
HTML 문서를 프린터로 인쇄하기
- 간단한 사진 이외에 복잡한 문서를 인쇄하기 위해서, 안드로이드는 HTML 형식의 문서를 출력할 수 있도록 지원해준다. Android 4.4 (API Level 19)부터 WebView에 관련 API를 제공하고 있다.
- HTML이 모두 로딩되는 시점을 알기 위해 WebViewClient
를 생성하고, WebView에 HTML 문서를 로딩시간다. 모두 로딩이 되지 않은 상태에서 인쇄를 하고자 시도하면, 비어있는 부분이 출력되는 등의 오동작을 하게 된다. 또한, WebView 객체는 출력이 완료되기까지 GC가 되면 안되므로 주의해야 한다. 아래는 관련 코드이다.
private WebView mWebView;
private void doWebViewPrint() {
// Create a WebView object specifically for printing
WebView webView = new WebView(getActivity());
webView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
@Override
public void onPageFinished(WebView view, String url) {
Log.i(TAG, "page finished loading " + url);
createWebPrintJob(view);
mWebView = null;
}
});
// Generate an HTML document on the fly:
String htmlDocument = "<html><body><h1>Test Content</h1><p>Testing, " +
"testing, testing...</p></body></html>";
webView.loadDataWithBaseURL(null, htmlDocument, "text/HTML", "UTF-8", null);
// Keep a reference to WebView object until you pass the PrintDocumentAdapter
// to the PrintManager
mWebView = webView;
}
- loadDataWithBaseURL() 에서 assets/ 의 내용을 불러오기 위해서는 아래와 같이 한다.
webView.loadDataWithBaseURL("file:///android_asset/images/", htmlBody,
"text/HTML", "UTF-8", null);
- 외부 URL에서 불러오기 위해서는 아래와 같이 한다.
// Print an existing web page (remember to request INTERNET permission!):
webView.loadUrl("http://developer.android.com/about/index.html");
- WebView를 통해 인쇄할때에는 아래와 같은 제약 사항이 있다.
- Header, Footer, Page Number등을 추가할 수 없다.
- 인쇄 페이지를 지정할 수 없다. (예, 10페이지중 2~4페이지만 출력)
- 한번에 하나의 인쇄만 진행할 수 있다.
- HTML문서에 CSS 인쇄 항목(Landscape 같은)등은 지원하지 않는다.
- 인쇄를 시작하도록 하기 위해 JavaScript를 사용할 수 없다.
private void createWebPrintJob(WebView webView) {
// Get a PrintManager instance
PrintManager printManager = (PrintManager) getActivity()
.getSystemService(Context.PRINT_SERVICE);
// Get a print adapter instance
PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter();
// Create a print job with name and adapter instance
String jobName = getString(R.string.app_name) + " Document";
PrintJob printJob = printManager.print(jobName, printAdapter,
new PrintAttributes.Builder().build());
// Save the job object for later status checking
mPrintJobs.add(printJob);
}
- 위에서 PrintJob
을 저장했는데, 이를 통해서 인쇄 진행 상태등을 확인할 수 있다. 인쇄를 진행하면서 Framework에서 알림표시를 하므로, 따로 앱에서 표시해줄 필요는 없다.
출처 : http://developer.android.com/
글
간단한 사진을 프린터로 인쇄하기
- Android 4.4 (API Level 19) 부터는 앱에서 바로 프린팅을 할 수 있는 Framework를 제공한다. Android Support Library v4에서도 프린팅을 위한 최소한의 API는 제공하고 있으므로, 그 이하 버전에서도 사용할 수 있다.
- Support Library의 PrintHelper
를 이용해 이미지를 프린트 하려면 아래와 같이 한다. setScaleMode()는 이미지를 인쇄 영역 내부에 출력(이미지 전체 출력, SCALE_MODE_FIT)할지, 이미지를 인쇄 영역에 꽉 채울지(이미지 일부가 잘릴 수 있음, SCALE_MODE_FILL) 선택한다. printBitmap()이 호출되면 Android 프린팅 UI가 표시되어 출력을 진행하므로, 더이상 해줄 것은 없다.
private void doPhotoPrint() {
PrintHelper photoPrinter = new PrintHelper(getActivity());
photoPrinter.setScaleMode(PrintHelper.SCALE_MODE_FIT);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.droids);
photoPrinter.printBitmap("droids.jpg - test print", bitmap);
}
글
카메라 제어
우선 카메라 Object를 얻어야 한다. Open을 할 때 UI Thread에서 하면 안된다.
다른 앱이 카메라를 사용중이거나 다른 문제가 있을 경우 아래 코드의 try에 걸리게 된다.
API Level 9 이상에서는 멀티 카메라를 지원하므로 ID값을 입력하지만, 그 이전 API에서는 open()하면 첫번째 뒷면 카메라의 Object가 나온다.private boolean safeCameraOpen(int id) {
boolean qOpened = false;
try {
releaseCameraAndPreview();
mCamera = Camera.open(id);
qOpened = (mCamera != null);
} catch (Exception e) {
Log.e(getString(R.string.app_name), "failed to open Camera");
e.printStackTrace();
}
return qOpened;
}
private void releaseCameraAndPreview() {
mPreview.setCamera(null);
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}카메라가 찍는 영상을 화면에 미리보기는 SurfaceView에 하게 되는데, 아래와 같이 구현해주면 된다.
class Preview extends ViewGroup implements SurfaceHolder.Callback {
SurfaceView mSurfaceView;
SurfaceHolder mHolder;
Preview(Context context) {
super(context);
mSurfaceView = new SurfaceView(context);
addView(mSurfaceView);
// Install a SurfaceHolder.Callback so we get notified when the
// underlying surface is created and destroyed.
mHolder = mSurfaceView.getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}}
(Preview Class 내부) setCamera()로 카메라 Object를 넘겨주면, startPreview()를 통해 미리보기가 시작된다.
public void setCamera(Camera camera) {
if (mCamera == camera) { return; }
stopPreviewAndFreeCamera();
mCamera = camera;
if (mCamera != null) {
List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
mSupportedPreviewSizes = localSizes;
requestLayout();
try {
mCamera.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
}
// Important: Call startPreview() to start updating the preview
// surface. Preview must be started before you can take a picture.
mCamera.startPreview();
}
}- 카메라 관련 설정(줌 Level 등)이 변경되면, 아래와 같이 setParameters()를 이용해 미리보기를 변경할 수 있다. 아래 예제는 미리보기 View 크기 변경임.
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
// Now that the size is known, set up the camera parameters and begin
// the preview.
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
requestLayout();
mCamera.setParameters(parameters);
// Important: Call startPreview() to start updating the preview surface.
// Preview must be started before you can take a picture.
mCamera.startPreview();
} 미리보기 방향을 변경하려면, setCameraDisplayOrientation() 메소드를 사용한다. API Level 14 이전에는 방향 변경전에 미리보기를 중지하고, 설정 후 재시작해야 한다.
아래 코드는 현재 화면과 같은 방향으로 미리보기를 표시하기 위한 방법이다.public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}
- 사진은
Camera.takePicture()
을 통해서 찍을 수 있다.Camera.PictureCallback과
Camera.ShutterCallback
으로 결과를 받는다. - 연속적으로 사진 찍기는
Camera.PreviewCallback
을 통해서 할 수 있다. - 사진을 찍은 후에는 미리보기가 멈추므로, 미리보기를 다시 시작해줘야 한다.
카메라 사용 후 Release는 미리보기 SurfaceView가 Destroy 될 때 하면 좋다. 또한, onPause()에서도 Release를 해야 하며, onResume()에서 다시 카메라를 Open해야 한다. 위 setCamera()처럼 카메라 초기화시에도 항상 미리보기 및 카메라를 Release해야 한다.
public void surfaceDestroyed(SurfaceHolder holder) {
// Surface will be destroyed when we return, so stop the preview.
if (mCamera != null) {
// Call stopPreview() to stop updating the preview surface.
mCamera.stopPreview();
}
}
/**
* When this function returns, mCamera will be null.
*/
private void stopPreviewAndFreeCamera() {
if (mCamera != null) {
// Call stopPreview() to stop updating the preview surface.
mCamera.stopPreview();
// Important: Call release() to release the camera for use by other
// applications. Applications should release the camera immediately
// during onPause() and re-open() it during onResume()).
mCamera.release();
mCamera = null;
}
}
출처 : http://developer.android.com/
글
간단히 동영상 찍기
- 사진 찍기와 동일한 퍼미션이 필요하다.
<manifest ... >
<uses-feature android:name="android.hardware.camera"
android:required="true" />
...
</manifest> - 동영상 촬영을 위해 아래와 같이 카메라 앱을 띄운다.
static final int REQUEST_VIDEO_CAPTURE = 1;
private void dispatchTakeVideoIntent() {
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
if (takeVideoIntent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE);
}
} - 카메라 앱에서 촬영한 영상을 VideoView에서 보기 위해서 아래와 같이 한다.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == RESULT_OK) {
Uri videoUri = intent.getData();
mVideoView.setVideoURI(videoUri);
}
}
출처 : http://developer.android.com/
글
간단히 사진 찍기
아래의 퍼미션이 필요하다.
<manifest ... >
<uses-feature android:name="android.hardware.camera"
android:required="true" />
...
</manifest>
- 만약 required를 false로 한다면, Runtime에 hasSystemFeature(PackageManager.FEATURE_CAMERA)를 통해 카메라가 사용 가능한지 알아볼 수 있다.
간단히 내장 카메라 앱을 통해 사진을 찍으려면 아래와 같이 한다. (resolveActivity 체크를 해야 앱이 죽는 걸 방지할 수 있다)
static final int REQUEST_IMAGE_CAPTURE = 1;
private void dispatchTakePictureIntent() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
}
}
(내장 카메라 앱) 썸네일을 얻기 위해서는 아래와 같이 결과를 받는다.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
Bundle extras = data.getExtras();
Bitmap imageBitmap = (Bitmap) extras.get("data");
mImageView.setImageBitmap(imageBitmap);
}
}- 다른 앱과 공유해도 되는 사진의 경우 아래 메소드로 얻을 수 있는 경로에 저장하는 것을 권장한다.
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); - 다른 앱과 공유하면 안되는 사진의 경우 getExternalFilesDir()로 얻을 수 있는 경로에 저장하는 것을 권장한다. 이 경로는 앱 삭제시에 함께 삭제된다.
- 외장 저장 장치의 경우 아래의 퍼미션이 필요하다. (Android 4.4 부터는 별도의 퍼미션이 필요없다. 타 앱에서 접근 하지 못함)
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
</manifest> - 파일명 중복을 방지하도록 임시 파일을 생성하려면, 아래와 같은 방식을 추천한다.
String mCurrentPhotoPath;
private File createImageFile() throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
// Save a file: path for use with ACTION_VIEW intents
mCurrentPhotoPath = "file:" + image.getAbsolutePath();
return image;
} - (내장 카메라 앱) 이제 전체 사진을 위에 만든 임시 파일에 받아오자!
static final in REQUEST_TAKE_PHOTO = 1;
private void dispatchTakePictureIntent() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// Ensure that there's a camera activity to handle the intent
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
// Create the File where the photo should go
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ex) {
// Error occurred while creating the File
...
}
// Continue only if the File was successfully created
if (photoFile != null) {
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(photoFile));
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
}
}
} - 한정된 메모리로 전체 사진을 로드하면, Out of Memory가 발생할 수 있다. 메모리 사용량을 줄이기 위해서 아래와 같은 방법을 사용하면 좋다.
private void setPic() {
// Get the dimensions of the View
int targetW = mImageView.getWidth();
int targetH = mImageView.getHeight();
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
// Determine how much to scale down the image
int scaleFactor = Math.min(photoW/targetW, photoH/targetH);
// Decode the image file into a Bitmap sized to fill the View
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
bmOptions.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
mImageView.setImageBitmap(bitmap);
}
출처 : http://developer.android.com/
글
오디오 제어
- 어떤 Audio Stream을 사용할지 선택한다. 일반적인 음악 재생은 STREAM_MUSIC을 선택한다. [다른 옵션 보기]
- 하드웨어 볼륨키는 현재 재생중인 것을 컨트롤한다. (재생중인 것이 없을 때는 전화벨 볼륨 조절)
특정 Audio Stream의 볼륨을 조절하고 싶다면 아래와 같이 한다. setVolumeControlStream(AudioManager.STREAM_MUSIC);
- 하드웨어 컨트롤키(블루투스 헤드셋등에 있는)를 제어하기 위해서는 아래와 같이 한다.
<receiver android:name=".RemoteControlReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>public class RemoteControlReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (KeyEvent.KEYCODE_MEDIA_PLAY == event.getKeyCode()) {
// Handle key press.
}
}
}
}만약, 코드상에서 Receiver를 등록/해제하려면 아래와 같이 한다.
AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...
// Start listening for button presses
am.registerMediaButtonEventReceiver(RemoteControlReceiver);
...
// Stop listening for button presses
am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);그런데, BroadcastReceiver는 일반적으로 Activity/Fragment등이 Invisible될 때 해제되므로 문제가 있다 (아래 내용에서 해결)
- 여러개의 앱들이 모두 음악을 재생하고자 하면 문제가 있기 때문에, 안드로이드 시스템에서 제어를 해야한다.
Focus를 가지고 오기 위해서는 아래와 같이 한다.
AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...
// Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
// Use the music stream.
AudioManager.STREAM_MUSIC,
// Request permanent focus.
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
am.registerMediaButtonEventReceiver(RemoteControlReceiver);
// Start playback.
}Focus를 반납하려면 아래와 같이 한다.
// Abandon audio focus when playback complete
am.abandonAudioFocus(afChangeListener)"Ducking" - Focus를 잃어도(다른 앱이 요청시) 계속 재생을 하고 싶다면 , Focus를 가져올때 아래와 같이 한다.
// Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
// Use the music stream.
AudioManager.STREAM_MUSIC,
// Request permanent focus.
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// Start playback.
}- Focus의 변화에 따라 아래와 같이 대응한다.
OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT
// Pause playback
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Resume playback
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
am.abandonAudioFocus(afChangeListener);
// Stop playback
}
}
}; - Duck 상태에서는 아래와 같이 대응한다.
OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Lower the volume
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Raise it back to normal
}
}
};
재생되는 오디오 기기에 대해
현재 재생 가능한 기기를 알아보기 위해서는 아래와 같이 한다. (AudioManager의 메소드)
if (isBluetoothA2dpOn()) {
// Adjust output for Bluetooth.
} else if (isSpeakerphoneOn()) {
// Adjust output for Speakerphone.
} else if (isWiredHeadsetOn()) {
// Adjust output for headsets
} else {
// If audio plays and noone can hear it, is it still playing?
}- 재생되던 기기의 변경이 일어났을 때 처리는 아래와 같이 한다. (특히, 이어폰을 빼거나 블루투스를 껐을때)
private class NoisyAudioStreamReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
// Pause the playback
}
}
}
private IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private void startPlayback() {
registerReceiver(myNoisyAudioStreamReceiver(), intentFilter);
}
private void stopPlayback() {
unregisterReceiver(myNoisyAudioStreamReceiver);
}
출처 : http://developer.android.com/