앞선  포스팅에서 토글 버튼을 눌러 실행하게 했던, QPairClipboardService를 구현해보겠습니다.


서비스 클래스 생성

다음처럼 서비스를 하나 만듭니다.


1
2
3
4
5
6
7
8
9
10
11
public class QPairClipboardService extends Service {
  @Override
  public IBinder onBind(Intent intent) {
      return null;
  }
 
  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
      return START_NOT_STICKY;
  }
}


지금부터 만들 서비스는 Started service로만 사용할 것이므로, 이 포스팅의 모든 코드는 특별히 언급이 없는 한, onStartCommand() 메소드 안에 입력한다는 것을 기억하세요!


클립보드 공유 서비스는 앱(토글 버튼을 가진 그 앱!)이 종료되어도 계속 동작해야 하므로 Foreground로 실행합니다. 서비스는 사용자로부터 서비스 시작 요청이 들어왔을 때, 다음처럼 자신을 Foreground로 실행할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
// start service for QPair clipboard
   if (ACTION_STARTSERVICE.equals(intent.getAction())) {
       Notification.Builder builder = new Notification.Builder(this);
       builder.setContentTitle(TAG_PEERCLIP)
              .setContentText("Service is running")
              .setTicker("QPair Clipboard started")
              .setSmallIcon(R.drawable.clipboard);
 
       startForeground(NOTI_ID, builder.build());
 
       Toast.makeText(this, "QPair clipboard service starts."
Toast.LENGTH_SHORT).show();
}


이제 앱에서 QPairClipboardService를 실행하면, 앱을 종료해도 서비스는 살아 있습니다. 살아 있다는 것은 Notification 바에 표시되고요.


서비스 구현

우리의 QPairClipboardService는 로컬 디바이스일 때와 피어 디바이스 일 때의 역할이 다릅니다.

  • 로컬 디바이스 일때: 클립보드를 감시하여 무언가가 복사될 때 QPair API를 이용하여 페어링된 기기에 클립보드 내용을 전달
  • 피어 디바이스 일때: QPair 메시지를 수신하면 클립보드에 입력

두 역할을 한 서비스에서 모두 구현해야겠지요.

 


 

로컬 디바이스 일때

 

일단 클립보드에 뭔가가 복사되었는지 확인하고 전달해야 하므로, 클립보드가 변경되었는지 확인해야 합니다.

그러려면 리스너를 등록해야 하는데요, 서비스가 시작될 때 등록하는 것이 좋겠군요.

위의 코드 블록에서 if문 다음에 다음 코드를 입력합니다.


1
2
3
4
5
6
7
8
9
final ClipboardManager clipBoard = 
(ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipBoard.addPrimaryClipChangedListener(
new ClipboardManager.OnPrimaryClipChangedListener() {
     @Override
    public void onPrimaryClipChanged() {
         saveClipItem(clipBoard.getPrimaryClip().getItemAt(0));
     }
 });


클립보드 변경이 일어났을 때 실행하는 saveClipItem() 메소드는, 먼저 클립보드 내용을 QPair property로 저장한 후, Peer Intent를 이용해 피어 디바이스로 클립보드 변경 알림을 보냅니다. 물론 Peer Intent에 클립보드 내용을 넣어 보낼 수도 있지만, QPair Property 도 써 볼겸 해서 이렇게 구현해봤습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void saveClipItem(ClipData.Item item) {
    if( item != null ) {
        updateContent(QPairClipItemUri, item.coerceToText(
QPairClipboardService.this).toString());
        sendDataToPeer();
    }
}
 
private void updateContent(String qPairClipItemUri, String clipdata) {
    if( qPairClipItemUri != null 
&& !qPairClipItemUri.isEmpty() 
&& clipdata != null ) {
        ContentValues cv = new ContentValues();
        cv.put(null, clipdata);
        getContentResolver().update(
Uri.parse(qPairClipItemUri), cv, null, null);
    }
}


sendToPeer() 메소드는 QPair 서비스에 바인딩하여 Peer Intent를 전송합니다.

우선 아래처럼 서비스에 바인딩하세요.

1
2
3
private void sendDataToPeer() {
    Intent intent = new Intent(QPairConstants.ACTION_SERVICE);
    bindService(intent.setPackage(QPairConstants.PACKAGE_NAME), new CopyClipboardServiceConnection(), 0);   

CopyClipboardServiceConnection는 ServiceConnection을 구현한 클래스입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyServiceConnection implements ServiceConnection {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        IPeerContext pContext = IPeerContext.Stub.asInterface(iBinder);
        if( pContext != null ) {
            IPeerIntent pi = null;
            try {
                pi = pContext.newPeerIntent();
                pi.setComponent(QPairClipboardService.this.getPackageName() + "/" +
                           QPairClipboardService.this.getClass().getName());
                pi.setAction(ACTION_CLIPDATA);
 
                IPeerIntent callback = pContext.newPeerIntent();
                callback.setAction(ACTION_CALLBACK);
 
                pContext.startServiceOnPeer(pi, callback, callback);
            } catch( RemoteException e ) {
                e.printStackTrace();
            }
       }
 
       unbindService(this);
    }
 
    @Override
    public void onServiceDisconnected(ComponentName componentName) { }

4번 줄부터 22번 줄까지는 QPair에서 Peer Intent를 전송하는 기본 코드입니다. Peer Intent를 쓴다고 하면 일단 위 코드를 똑같이 쓴 후 고치면 돼요. :)

우선 아규먼트로 받은 바인더로부터 Peer Context를 가져온 후, 여기서 Peer Intent를 생성합니다.

그런 다음 보통 Intent를 설정하듯, 처리할 컴포넌트와 전달할 값 등을 입력하고 전송합니다. 이 Peer Intent는 피어 기기에 있는 QPairClipboardService를 실행하는 용도이니, startServiceOnPeer()로 전달해야겠군요.


13~14번 줄은 Peer Intent의 전송과 처리 결과를 수신하기 위해 만든 callback Intent입니다. QPair API revision 1에서는 오류가 발생했을 때만 전달되지만, revision 2부터는 성공했을 때도 callback을 받을 수 있게 바뀌었어요.

startServiceOnPeer()의 두번째 파라미터가 성공했을 때의 callback이고, 세번째 파라미터가 실패했을 때의 callback입니다. 이 코드에서는 성공 및 실패 callback을 같은 인텐트로 사용했습니다(귀차니즘일수도...). callback intent를 처리하는 코드는 그냥 BroadcastReceiver 사용하면 되니, 코드는 생략하겠습니다^^;


이렇게만 하면 기본적인 클립보드 공유가 가능합니다.

물론 앱을 좀 더 멋지게 만들려면, 이미지 같이 웹이나 로컬 디바이스에 저장된 파일을 클립보드에 복사하여 공유하고 싶을 수도 있겠죠. 이 내용은 다음에 써 볼게요.


피어 디바이스 일때


로컬 디바이스에서 전송한 Peer Intent는 자동적으로 피어 디바이스의 QPairClipboardService를 실행합니다. 실행된 서비스는 Peer Intent가 가진 정보를 일반 Intent로 수신하겠죠. 그 Intent를 처리하는 것도 onStartCommand() 메소드가 해야 합니다.

다음처럼 onStartCommand()에 새로운 Intent 액션을 처리하는 코드를 추가하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if( intent != null ) {
        Log.d("test", "onStartCommand: " + intent.getAction());
 
        // start service for QPair clipboard
        if (ACTION_STARTSERVICE.equals(intent.getAction())) {
            ...
        } else if (ACTION_CLIPDATA.equals(intent.getAction())) {
            putClipItem();
            Toast.makeText(this
"QPair clipboard service gets a peer clip.",
Toast.LENGTH_SHORT).show();
        }
    }
    return START_NOT_STICKY;
}


putClipItem() 메소드는 로컬 디바이스의 클립보드 내용을 QPair property에서 가져온 후 클립보드에 입력합니다.


1
2
3
4
5
6
7
8
9
10
private void putClipItem() {
  final ClipboardManager clipBoard = 
(ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
 
  // read the Clip item
  String content = getContent(QPairClipItemUriPeer);
  ClipData clip = ClipData.newPlainText(TAG_PEERCLIP, content);
  if( clip != null )
      clipBoard.setPrimaryClip(clip);
}


자, 이제 구현이 끝났군요!


반복 복사 제거


지금까지대로 구현하면 아무 문제 없이 잘 동작할 것 같지만, 한 가지 문제가 있습니다.

연결된 두 디바이스에서 사용자가 각각 QPairClipboardService를 실행하면 어떻게 될까요?


디바이스 A에서 사용자가 무언가를 복사하면, 서비스가 복사 내용을 연결된 디바이스 B로 전달할 겁니다. 디바이스 B는 전달받은 내용을 클립보드에 입력합니다. 그러면 당연히 클립보드가 변경되므로, 디바이스 B의 QPairClipboardService는 복사된 내용을 디바이스 A로 전달합니다. 디바이스 A는 그 내용을 클립보드에 입력합니다. 당연히 클립보드가 변경되므로 디바이스 A의 QPairClipboardService가….

이렇게 클립보드 복사가 무한 반복되는 일이 발생합니다.


이 무서운 현상을 피하기 위해, saveClipItem()을 호출하기 전에 ‘사용자’가 복사한 것인지, ‘QPairClipboardService’가 복사한 것인지 확인하는 작업이 필요합니다.


아래의 코드를,


1
2
3
4
5
6
7
8
9
final ClipboardManager clipBoard = 
(ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipBoard.addPrimaryClipChangedListener(
new ClipboardManager.OnPrimaryClipChangedListener() {
     @Override
    public void onPrimaryClipChanged() {
         saveClipItem(clipBoard.getPrimaryClip().getItemAt(0));
     }
 });


다음과 같이 변경합니다.


1
2
3
4
5
6
7
8
9
10
11
12
final ClipboardManager clipBoard = 
(ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipBoard.addPrimaryClipChangedListener(
new ClipboardManager.OnPrimaryClipChangedListener() {
     @Override
    public void onPrimaryClipChanged() {
         if (!TAG_PEERCLIP.equals(
clipBoard.getPrimaryClipDescription().getLabel())) {
            saveClipItem(clipBoard.getPrimaryClip().getItemAt(0));
         }
    }
});


이렇게 하면 QPair SDK를 사용한 클립보드 공유 기능이 잘 동작합니다. 

전체 소스는 http://smartworldcampus.lge.com/116 에서 받으실 수 있습니다~


실행 화면은 이렇게!

 

local device에서 복사

 

 

피어 디바이스의 클립보드에 복사된 모습



저작자 표시 비영리 변경 금지
신고