고급 주제

이 섹션은 참고용이며 꼭 맨 위부터 읽을 필요는 없습니다.

프레임워크 API 사용:

이러한 API는 더 일관된 API 노출 영역 (예: UserHandle 객체 방지)을 위해 SDK에서 래핑되지만 지금은 이를 직접 호출할 수 있습니다.

구현은 간단합니다. 상호작용할 수 있으면 계속 진행합니다. 그렇지 않지만 요청할 수 있는 경우 사용자 메시지/배너/도움말 등을 표시합니다. 사용자가 설정으로 이동하는 데 동의하면 요청 인텐트를 만들고 Context#startActivity를 사용하여 사용자를 설정으로 보냅니다. 브로드캐스트를 사용하여 이 기능이 변경될 때 감지하거나 사용자가 다시 돌아왔을 때 다시 확인하면 됩니다.

이를 테스트하려면 직장 프로필에서 TestDPC를 열고 맨 아래로 이동하여 연결된 앱 허용 목록에 패키지 이름을 추가하도록 선택합니다. 이렇게 하면 관리자가 앱을 '허용 목록'에 추가한 것처럼 보입니다.

용어 설명

이 섹션에서는 교차 프로필 개발과 관련된 주요 용어를 정의합니다.

교차 프로필 구성

교차 프로필 구성은 관련 교차 프로필 제공업체 클래스를 그룹화하고 교차 프로필 기능에 대한 일반 구성을 제공합니다. 일반적으로 코드베이스당 하나의 @CrossProfileConfiguration 주석이 있지만 일부 복잡한 애플리케이션에서는 여러 개가 있을 수 있습니다.

프로필 커넥터

커넥터는 프로필 간의 연결을 관리합니다. 일반적으로 각 교차 프로필 유형은 특정 커넥터를 가리킵니다. 단일 구성의 모든 교차 프로필 유형은 동일한 커넥터를 사용해야 합니다.

교차 프로필 제공업체 클래스

교차 프로필 제공업체 클래스는 관련 교차 프로필 유형을 그룹화합니다.

Mediator

미디에이터는 상위 코드와 하위 코드 사이에 있으며 올바른 프로필에 호출을 배포하고 결과를 병합합니다. 이 코드만 프로필을 인식해야 합니다. 이는 SDK에 내장된 것이 아니라 아키텍처 개념입니다.

교차 프로필 유형

교차 프로필 유형은 @CrossProfile 주석이 지정된 메서드가 포함된 클래스 또는 인터페이스입니다. 이 유형의 코드는 프로필을 인식할 필요가 없으며 로컬 데이터에만 작동하는 것이 이상적입니다.

프로필 유형

프로필 종류
현재실행 중인 활성 프로필입니다.
기타(있는 경우) 실행하지 않는 프로필입니다.
개인사용자 0: 기기에서 사용 중지할 수 없는 프로필입니다.
업무일반적으로 사용자 10명이지만 그보다 많을 수 있으며, 사용 설정 및 사용 중지할 수 있으며, 직장 앱과 데이터를 포함하는 데 사용됩니다.
기본애플리케이션에서 정의할 수 있습니다. 두 프로필의 병합된 보기를 보여주는 프로필입니다.
보조primary가 정의된 경우 secondary는 기본이 아닌 프로필입니다.
공급업체기본 프로필의 공급자는 두 프로필 모두이고 보조 프로필의 공급자는 보조 프로필 자체뿐입니다.

프로필 식별자

프로필 유형 (개인 또는 직장)을 나타내는 클래스입니다. 이는 여러 프로필에서 실행되는 메서드에 의해 반환되며 이러한 프로필에서 더 많은 코드를 실행하는 데 사용할 수 있습니다. 편리한 저장을 위해 int에 직렬화할 수 있습니다.

이 가이드에서는 Android 앱 내에서 효율적이고 유지 관리 가능한 교차 프로필 기능을 빌드하기 위한 권장 구조를 설명합니다.

CrossProfileConnector를 싱글톤으로 변환

애플리케이션의 전체 수명 주기 동안 단일 인스턴스만 사용해야 합니다. 그렇지 않으면 병렬 연결이 생성됩니다. Dagger와 같은 종속 항목 삽입 프레임워크를 사용하거나 새 클래스 또는 기존 클래스에서 기존 싱글톤 패턴을 사용하여 이를 실행할 수 있습니다.

메서드에서 생성하는 대신 호출 시 클래스에 생성된 Profile 인스턴스를 삽입하거나 전달합니다.

이렇게 하면 나중에 단위 테스트에서 자동으로 생성된 FakeProfile 인스턴스를 전달할 수 있습니다.

미디에이터 패턴 고려

이 일반적인 패턴은 기존 API 중 하나 (예: getEvents())를 모든 호출자에 대해 프로필 인식하도록 만드는 것입니다. 이 경우 기존 API는 생성된 교차 프로필 코드에 대한 새 호출이 포함된 '미디에이터' 메서드 또는 클래스가 될 수 있습니다.

이렇게 하면 모든 호출자가 교차 프로필 호출을 수행하도록 강요하지 않아도 API의 일부가 됩니다.

제공업체에서 구현 클래스를 노출하지 않아도 되도록 인터페이스 메서드에 @CrossProfile로 주석을 지정하는 것이 좋습니다.

이는 종속 항목 삽입 프레임워크와 잘 작동합니다.

교차 프로필 호출에서 데이터를 수신하는 경우 데이터가 어느 프로필에서 가져왔는지 참조하는 필드를 추가하는 것이 좋습니다.

UI 레이어에서 이를 알고 싶을 수 있으므로(예: 작업 항목에 배지 아이콘 추가) 좋은 방법입니다. 패키지 이름과 같이 데이터 식별자가 이 없이 더 이상 고유하지 않은 경우에도 필요할 수 있습니다.

교차 프로필

이 섹션에서는 교차 프로필 상호작용을 직접 빌드하는 방법을 간략히 설명합니다.

기본 프로필

이 문서의 예시에서 대부분의 호출에는 직장, 개인, 둘 다를 포함하여 실행할 프로필에 관한 명시적인 안내가 포함되어 있습니다.

실제로 하나의 프로필에만 환경이 병합된 앱의 경우 이 결정을 실행 중인 프로필에 종속시키는 것이 좋습니다. 따라서 코드베이스에 if-else 프로필 조건문이 널려 있는 것을 방지하기 위해 이를 고려하는 유사한 편리한 메서드도 있습니다.

커넥터 인스턴스를 만들 때 '기본' 프로필 유형(예: 'WORK')을 지정할 수 있습니다. 이렇게 하면 다음과 같은 추가 옵션을 사용할 수 있습니다.

profileCalendarDatabase.primary().getEvents();

profileCalendarDatabase.secondary().getEvents();

// Runs on all profiles if running on the primary, or just
// on the current profile if running on the secondary.
profileCalendarDatabase.suppliers().getEvents();

교차 프로필 유형

@CrossProfile 주석이 지정된 메서드가 포함된 클래스와 인터페이스를 교차 프로필 유형이라고 합니다.

교차 프로필 유형의 구현은 실행 중인 프로필과 같은 프로필과 독립적이어야 합니다. 다른 메서드를 호출할 수 있으며 일반적으로 단일 프로필에서 실행되는 것처럼 작동해야 합니다. 자체 프로필의 상태에만 액세스할 수 있습니다.

교차 프로필 유형의 예:

public class Calculator {
  @CrossProfile
  public int add(int a, int b) {
    return a + b;
  }
}

수업 주석

가장 강력한 API를 제공하려면 다음과 같이 각 교차 프로필 유형의 커넥터를 지정해야 합니다.

@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
  @CrossProfile
  public int add(int a, int b) {
    return a + b;
  }
}

이는 선택사항이지만 생성된 API가 유형에 더 구체적이고 컴파일 시간 검사에 더 엄격해진다는 의미입니다.

인터페이스

인터페이스의 메서드에 @CrossProfile로 주석을 달면 프로필 전반에서 액세스할 수 있어야 하는 이 메서드의 구현이 있을 수 있음을 나타냅니다.

교차 프로필 제공업체에서 교차 프로필 인터페이스의 구현을 반환할 수 있으며, 이렇게 하면 이 구현에 교차 프로필에서 액세스할 수 있어야 함을 나타냅니다. 구현 클래스에 주석을 달 필요는 없습니다.

교차 프로필 제공업체

모든 교차 프로필 유형@CrossProfileProvider 주석이 달린 메서드에서 제공해야 합니다. 이러한 메서드는 교차 프로필 호출이 실행될 때마다 호출되므로 각 유형에 대해 싱글톤을 유지하는 것이 좋습니다.

생성자

제공업체에는 인수를 사용하지 않거나 단일 Context 인수를 사용하는 공개 생성자가 있어야 합니다.

제공자 메서드

제공자 메서드는 인수를 사용하지 않거나 단일 Context 인수를 사용해야 합니다.

종속 항목 삽입

Dagger와 같은 종속 항목 삽입 프레임워크를 사용하여 종속 항목을 관리하는 경우 일반적으로 했던 것처럼 프레임워크에서 교차 프로필 유형을 만든 다음 이러한 유형을 제공업체 클래스에 삽입하는 것이 좋습니다. 그러면 @CrossProfileProvider 메서드가 이러한 삽입된 인스턴스를 반환할 수 있습니다.

프로필 커넥터

각 교차 프로필 구성에는 다른 프로필에 대한 연결을 관리하는 단일 프로필 커넥터가 있어야 합니다.

기본 프로필 커넥터

코드베이스에 교차 프로필 구성이 하나만 있는 경우 자체 프로필 커넥터를 만들지 않고 com.google.android.enterprise.connectedapps.CrossProfileConnector를 사용할 수 있습니다. 지정되지 않은 경우 사용되는 기본값입니다.

교차 프로필 커넥터를 구성할 때 빌더에서 다음과 같은 옵션을 지정할 수 있습니다.

  • 예약된 실행자 서비스

    SDK에서 만든 스레드를 제어하려면 #setScheduledExecutorService()를 사용하세요.

  • 바인더

    프로필 결합과 관련하여 구체적인 요구사항이 있는 경우 #setBinder를 사용하세요. 이는 기기 정책 컨트롤러에서만 사용할 가능성이 높습니다.

맞춤 프로필 커넥터

일부 구성을 설정하려면(CustomProfileConnector 사용) 맞춤 프로필 커넥터가 필요하며 단일 코드베이스에 여러 커넥터가 필요한 경우 (예: 프로세스가 여러 개인 경우 프로세스당 하나의 커넥터 권장)에도 맞춤 프로필 커넥터가 필요합니다.

ProfileConnector를 만들면 다음과 같이 표시됩니다.

@GeneratedProfileConnector
public interface MyProfileConnector extends ProfileConnector {
  public static MyProfileConnector create(Context context) {
    // Configuration can be specified on the builder
    return GeneratedMyProfileConnector.builder(context).build();
  }
}
  • serviceClassName

    생성된 서비스의 이름 (AndroidManifest.xml에서 참조해야 함)을 변경하려면 serviceClassName=를 사용합니다.

  • primaryProfile

    기본 프로필을 지정하려면 primaryProfile을 사용합니다.

  • availabilityRestrictions

    SDK가 연결 및 프로필 가용성에 적용하는 제한사항을 변경하려면 availabilityRestrictions를 사용하세요.

기기 정책 컨트롤러

앱이 기기 정책 컨트롤러인 경우 DeviceAdminReceiver를 참조하는 DpcProfileBinder 인스턴스를 지정해야 합니다.

자체 프로필 커넥터를 구현하는 경우:

@GeneratedProfileConnector
public interface DpcProfileConnector extends ProfileConnector {
  public static DpcProfileConnector get(Context context) {
    return GeneratedDpcProfileConnector.builder(context).setBinder(new
DpcProfileBinder(new ComponentName("com.google.testdpc",
"AdminReceiver"))).build();
  }
}

또는 기본 CrossProfileConnector를 사용합니다.

CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();

교차 프로필 구성

@CrossProfileConfiguration 주석은 메서드 호출을 올바르게 전달하기 위해 커넥터를 사용하여 모든 교차 프로필 유형을 서로 연결하는 데 사용됩니다. 이렇게 하려면 다음과 같이 모든 제공자를 가리키는 @CrossProfileConfiguration로 클래스에 주석을 추가합니다.

@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}

이렇게 하면 모든 교차 프로필 유형에 동일한 프로필 커넥터가 있거나 지정된 커넥터가 없는지 확인할 수 있습니다.

  • serviceSuperclass

    기본적으로 생성된 서비스는 android.app.Service를 상위 클래스로 사용합니다. 슈퍼클래스로 다른 클래스 (자체가 android.app.Service의 서브클래스여야 함)를 사용해야 하는 경우 serviceSuperclass=을 지정합니다.

  • serviceClass

    지정하면 서비스가 생성되지 않습니다. 이는 사용 중인 프로필 커넥터의 serviceClassName와 일치해야 합니다. 맞춤 서비스는 생성된 _Dispatcher 클래스를 사용하여 다음과 같이 호출을 전달해야 합니다.

public final class TestProfileConnector_Service extends Service {
  private Stub binder = new Stub() {
    private final TestProfileConnector_Service_Dispatcher dispatcher = new
TestProfileConnector_Service_Dispatcher();

    @Override
    public void prepareCall(long callId, int blockId, int numBytes, byte[] params)
{
      dispatcher.prepareCall(callId, blockId, numBytes, params);
    }

    @Override
    public byte[] call(long callId, int blockId, long crossProfileTypeIdentifier,
int methodIdentifier, byte[] params,
    ICrossProfileCallback callback) {
      return dispatcher.call(callId, blockId, crossProfileTypeIdentifier,
methodIdentifier, params, callback);
    }

    @Override
    public byte[] fetchResponse(long callId, int blockId) {
      return dispatcher.fetchResponse(callId, blockId);
  };

  @Override
  public Binder onBind(Intent intent) {
    return binder;
  }
}

교차 프로필 호출 전후에 추가 작업을 실행해야 하는 경우에 사용할 수 있습니다.

  • 커넥터

    기본 CrossProfileConnector 이외의 커넥터를 사용하는 경우 connector=를 사용하여 지정해야 합니다.

공개 상태

교차 프로필과 상호작용하는 애플리케이션의 모든 부분에서 프로필 커넥터를 볼 수 있어야 합니다.

@CrossProfileConfiguration 주석이 추가된 클래스는 애플리케이션에 사용되는 모든 제공자를 볼 수 있어야 합니다.

동기 호출

커넥티드 앱 SDK는 불가피한 경우 동기 (차단) 호출을 지원합니다. 하지만 이러한 호출을 사용하면 여러 가지 단점 (예: 호출이 장시간 차단될 수 있음)이 있으므로 가능한 경우 동기식 호출을 피하는 것이 좋습니다. 비동기 호출 사용에 관한 자세한 내용은 비동기 호출을 참고하세요 .

연결 소유자

동기식 호출을 사용하는 경우 교차 프로필 호출을 실행하기 전에 등록된 연결 홀더가 있는지 확인해야 합니다. 그러지 않으면 예외가 발생합니다. 자세한 내용은 연결 소유자를 참고하세요.

연결 홀더를 추가하려면 객체 (예: 교차 프로필 호출을 실행하는 객체 인스턴스)와 함께 ProfileConnector#addConnectionHolder(Object)를 호출합니다. 이렇게 하면 이 객체가 연결을 사용하고 있음을 기록하고 연결을 시도합니다. 동기식 호출을 실행하기 전에 호출해야 합니다. 비차단 호출이므로 호출 시 연결이 준비되지 않았거나 연결할 수 없는 경우 (이 경우 일반적인 오류 처리 동작이 적용됨)

ProfileConnector#addConnectionHolder(Object)를 호출할 때 적절한 교차 프로필 권한이 없거나 연결할 프로필이 없는 경우 오류가 발생하지는 않지만 연결된 콜백은 호출되지 않습니다. 나중에 권한이 부여되거나 다른 프로필을 사용할 수 있게 되면 연결이 설정되고 콜백이 호출됩니다.

또는 ProfileConnector#connect(Object)는 객체를 연결 홀더로 추가하고 연결을 설정하거나 UnavailableProfileException을 발생시키는 차단 메서드입니다. 이 메서드는 UI 스레드에서 호출할 수 없습니다.

ProfileConnector#connect(Object) 및 유사한 ProfileConnector#connect 호출은 닫히면 연결 홀더를 자동으로 삭제하는 자동 닫기 객체를 반환합니다. 이를 통해 다음과 같은 용도로 사용할 수 있습니다.

try (ProfileConnectionHolder p = connector.connect()) {
  // Use the connection
}

동기 호출을 완료하면 ProfileConnector#removeConnectionHolder(Object)를 호출해야 합니다. 모든 연결 소유자가 삭제되면 연결이 닫힙니다.

연결

연결 리스너는 연결 상태가 변경될 때 알림을 받기 위해 사용할 수 있으며 connector.utils().isConnected는 연결이 있는지 확인하는 데 사용할 수 있습니다. 예를 들면 다음과 같습니다.

// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
  if (crossProfileConnector.utils().isConnected()) {
    // Make cross-profile calls.
  }
});

비동기 호출

프로필 경계를 통해 노출되는 모든 메서드는 차단(동기) 또는 비차단 (비동기)으로 지정해야 합니다. 비동기 데이터 유형 (예: ListenableFuture)을 반환하거나 콜백 매개변수를 허용하는 모든 메서드는 비차단으로 표시됩니다. 다른 모든 메서드는 차단으로 표시됩니다.

비동기 호출을 사용하는 것이 좋습니다. 동기 호출을 사용해야 하는 경우 동기 호출을 참고하세요.

콜백

비차단 호출의 가장 기본적인 유형은 결과와 함께 호출할 메서드가 포함된 인터페이스를 매개변수 중 하나로 허용하는 void 메서드입니다. 이러한 인터페이스가 SDK와 함께 작동하려면 인터페이스에 @CrossProfileCallback 주석을 지정해야 합니다. 예를 들면 다음과 같습니다.

@CrossProfileCallback
public interface InstallationCompleteListener {
  void installationComplete(int state);
}

그러면 이 인터페이스를 @CrossProfile 주석이 달린 메서드의 매개변수로 사용하고 평소와 같이 호출할 수 있습니다. 예를 들면 다음과 같습니다.

@CrossProfile
public void install(String filename, InstallationCompleteListener callback) {
  // Do something on a separate thread and then:
  callback.installationComplete(1);
}

// In the mediator
profileInstaller.work().install(filename, (status) -> {
  // Deal with callback
}, (exception) -> {
  // Deal with possibility of profile unavailability
});

이 인터페이스에 매개변수가 0개 또는 1개인 단일 메서드가 포함된 경우 한 번에 여러 프로필을 호출하는 데도 사용할 수 있습니다.

콜백을 사용하여 임의 개수의 값을 전달할 수 있지만 연결은 첫 번째 값에 대해서만 열려 유지됩니다. 더 많은 값을 수신하기 위해 연결을 열어 두는 방법에 관한 자세한 내용은 연결 홀더를 참고하세요.

콜백이 있는 동기 메서드

SDK에서 콜백을 사용하는 한 가지 특이한 점은 기술적으로 콜백을 사용하는 동기식 메서드를 작성할 수 있다는 것입니다.

public void install(InstallationCompleteListener callback) {
  callback.installationComplete(1);
}

이 경우 콜백이 있더라도 메서드는 실제로 동기식입니다. 다음 코드는 올바르게 실행됩니다.

System.out.println("This prints first");
installer.install(() -> {
        System.out.println("This prints second");
});
System.out.println("This prints third");

그러나 SDK를 사용하여 호출하면 동일한 방식으로 작동하지 않습니다. 'This prints third'가 출력되기 전에 install 메서드가 호출되었다고 보장할 수 없습니다. SDK에서 비동기식으로 표시된 메서드를 사용하는 경우 메서드가 호출되는 시점에 관해 가정해서는 안 됩니다.

간단한 콜백

'간단한 콜백'은 교차 프로필 호출 시 추가 기능을 허용하는 더 제한적인 형태의 콜백입니다. 간단한 인터페이스는 매개변수를 0개 또는 1개 받을 수 있는 단일 메서드를 포함해야 합니다.

@CrossProfileCallback 주석에 simple=true를 지정하여 콜백 인터페이스가 유지되어야 한다고 적용할 수 있습니다.

간단한 콜백은 .both(), .suppliers()와 같은 다양한 메서드에서 사용할 수 있습니다.

연결 소유자

비동기 호출 (콜백 또는 future 사용)을 실행하면 호출 시 연결 홀더가 추가되고 예외 또는 값이 전달되면 삭제됩니다.

콜백을 사용하여 두 개 이상의 결과가 전달될 것으로 예상되는 경우 콜백을 연결 홀더로 수동으로 추가해야 합니다.

MyCallback b = //...
connector.addConnectionHolder(b);

  profileMyClass.other().registerListener(b);

  // Now the connection will be held open indefinitely, once finished:
  connector.removeConnectionHolder(b);

try-with-resources 블록과 함께 사용할 수도 있습니다.

MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
  profileMyClass.other().registerListener(b);

  // Other things running while we expect results
}

콜백 또는 future를 사용하여 호출하면 결과가 전달될 때까지 연결이 열려 유지됩니다. 결과가 전달되지 않을 것으로 판단되면 콜백 또는 future를 연결 홀더로 삭제해야 합니다.

connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);

자세한 내용은 연결 소유자를 참고하세요.

Future

SDK는 Futures도 기본적으로 지원합니다. 기본적으로 지원되는 유일한 Future 유형은 ListenableFuture이지만 맞춤 Future 유형을 사용할 수도 있습니다. future를 사용하려면 지원되는 Future 유형을 교차 프로필 메서드의 반환 유형으로 선언한 다음 평소와 같이 사용하면 됩니다.

이는 콜백과 동일한 '비정상적인 기능'을 갖습니다. future를 반환하는 동기 메서드 (예: immediateFuture 사용)는 현재 프로필에서 실행될 때와 다른 프로필에서 실행될 때 다르게 동작합니다. SDK에서 비동기식으로 표시된 메서드를 사용하는 경우 메서드가 호출되는 시점에 관해 가정해서는 안 됩니다.

스레드

기본 스레드에서 교차 프로필 future 또는 콜백의 결과를 차단하지 마세요. 스레드 이렇게 하면 경우에 따라 코드가 무한정 차단됩니다. 이는 다른 프로필과의 연결도 메인 스레드에서 설정되기 때문입니다. 교차 프로필 결과를 기다리는 동안 차단되면 연결이 설정되지 않습니다.

가용성

사용 가능 여부 리스너는 사용 가능 여부 상태가 변경될 때 알림을 받기 위해 사용할 수 있으며 connector.utils().isAvailable는 다른 프로필을 사용할 수 있는지 확인하는 데 사용할 수 있습니다. 예를 들면 다음과 같습니다.

crossProfileConnector.registerAvailabilityListener(() -> {
  if (crossProfileConnector.utils().isAvailable()) {
    // Show cross-profile content
  } else {
    // Hide cross-profile content
  }
});

연결 소유자

연결 소유자는 설정되고 유지되는 교차 프로필 연결에 관심이 있는 것으로 기록되는 임의의 객체입니다.

기본적으로 비동기 호출을 실행하면 호출이 시작될 때 연결 홀더가 추가되고 결과나 오류가 발생하면 삭제됩니다.

연결 홀더를 수동으로 추가 및 삭제하여 연결을 더 세부적으로 제어할 수도 있습니다. 연결 소유자는 connector.addConnectionHolder를 사용하여 추가하고 connector.removeConnectionHolder를 사용하여 삭제할 수 있습니다.

연결 홀더가 하나 이상 추가되면 SDK는 연결을 유지하려고 시도합니다. 연결 홀더가 추가되지 않은 경우 연결을 닫을 수 있습니다.

추가한 연결 홀더에 대한 참조를 유지하고 더 이상 관련이 없으면 삭제해야 합니다.

동기 호출

동기식 호출을 하기 전에 연결 홀더를 추가해야 합니다. 이는 어떤 객체를 사용하여도 되지만 더 이상 동기식 호출을 실행할 필요가 없을 때 삭제할 수 있도록 해당 객체를 추적해야 합니다.

비동기 호출

비동기 호출을 실행하면 호출과 첫 번째 응답 또는 오류 간에 연결이 열리도록 연결 홀더가 자동으로 관리됩니다. 그 이후에도 연결이 유지되어야 하는 경우 (예: 단일 콜백을 사용하여 여러 응답을 수신하는 경우) 콜백 자체를 연결 홀더로 추가하고 더 이상 데이터를 수신할 필요가 없으면 콜백을 삭제해야 합니다.

오류 처리

기본적으로 다른 프로필을 사용할 수 없는 경우 다른 프로필을 호출하면 UnavailableProfileException이 발생하거나 Future에 전달되거나 비동기 호출의 오류 콜백이 발생합니다.

이를 방지하려면 개발자가 #both() 또는 #suppliers()를 사용하고 결과 목록의 항목 수를 처리하는 코드를 작성하면 됩니다. 이 수는 다른 프로필을 사용할 수 없는 경우 1이고 사용할 수 있는 경우 2입니다.

예외

현재 프로필을 호출한 후에 발생하는 확인되지 않은 예외는 평소와 같이 전파됩니다. 이는 호출하는 데 사용된 메서드 (#current(), #personal, #both 등)와 관계없이 적용됩니다.

다른 프로필을 호출한 후에 발생하는 확인되지 않은 예외는 원래 예외를 원인으로 하여 ProfileRuntimeException이 발생합니다. 이는 호출하는 데 사용된 메서드 (#other(), #personal, #both 등)와 관계없이 적용됩니다.

ifAvailable

UnavailableProfileException 인스턴스를 포착하고 처리하는 대신 .ifAvailable() 메서드를 사용하여 UnavailableProfileException을 발생시키는 대신 반환할 기본값을 제공할 수 있습니다.

예를 들면 다음과 같습니다.

profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);

테스트

코드를 테스트 가능하게 하려면 프로필 커넥터를 사용하는 모든 코드 (프로필 사용 가능 여부를 확인하거나 수동으로 연결하는 등)에 프로필 커넥터의 인스턴스를 삽입해야 합니다. 또한 프로필 인식 유형의 인스턴스가 사용되는 위치에 이를 삽입해야 합니다.

테스트에 사용할 수 있는 커넥터 및 유형의 가짜가 제공됩니다.

먼저 테스트 종속 항목을 추가합니다.

  testAnnotationProcessor
'com.google.android.enterprise.connectedapps:connectedapps-processor:1.1.2'
  testCompileOnly
'com.google.android.enterprise.connectedapps:connectedapps-testing-annotations:1.1.2'
  testImplementation
'com.google.android.enterprise.connectedapps:connectedapps-testing:1.1.2'

그런 다음 테스트 클래스에 @CrossProfileTest 주석을 추가하여 테스트할 @CrossProfileConfiguration 주석이 달린 클래스를 식별합니다.

@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {

}

이렇게 하면 구성에 사용된 모든 유형과 커넥터의 가짜가 생성됩니다.

테스트에서 이러한 가짜의 인스턴스를 만듭니다.

private final FakeCrossProfileConnector connector = new
FakeCrossProfileConnector();
private final NotesManager personalNotesManager = new NotesManager(); //
real/mock/fake
private final NotesManager workNotesManager = new NotesManager(); // real/mock/fake

private final FakeProfileNotesManager profileNotesManager =
  FakeProfileNotesManager.builder()
    .personal(personalNotesManager)
    .work(workNotesManager)
    .connector(connector)
    .build();

프로필 상태를 설정합니다.

connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();

가짜 커넥터와 교차 프로필 클래스를 테스트 대상 코드에 전달한 다음 호출합니다.

호출은 올바른 타겟으로 라우팅되며 연결 해제되었거나 사용할 수 없는 프로필을 호출할 때 예외가 발생합니다.

지원되는 유형

다음 유형은 별도의 작업 없이 지원됩니다. 이러한 유형은 모든 교차 프로필 호출의 인수 또는 반환 유형으로 사용할 수 있습니다.

  • 프리미티브 (byte, short, int, long, float, double, char, boolean)
  • 박스형 원시 (java.lang.Byte, java.lang.Short, java.lang.Integer, java.lang.Long, java.lang.Float, java.lang.Double, java.lang.Character, java.lang.Boolean, java.lang.Void)
  • java.lang.String
  • android.os.Parcelable를 구현하는 모든 항목
  • java.io.Serializable를 구현하는 모든 항목
  • 단일 차원 비기본 배열
  • java.util.Optional
  • java.util.Collection
  • java.util.List
  • java.util.Map
  • java.util.Set
  • android.util.Pair
  • com.google.common.collect.ImmutableMap.

지원되는 모든 제네릭 유형 (예: java.util.Collection)은 지원되는 유형을 유형 매개변수로 가질 수 있습니다. 예를 들면 다음과 같습니다.

java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>>은(는) 유효한 유형입니다.

Future

다음 유형은 반환 유형으로만 지원됩니다.

  • com.google.common.util.concurrent.ListenableFuture

맞춤 Parcelable 래퍼

유형이 위 목록에 없는 경우 먼저 android.os.Parcelable 또는 java.io.Serializable를 올바르게 구현하도록 만들 수 있는지 고려합니다. 그러면 유형에 대한 지원을 추가할 parcelable 래퍼를 볼 수 없습니다.

맞춤 Future 래퍼

이전 목록에 없는 future 유형을 사용하려면 future 래퍼를 참고하여 지원을 추가하세요.

Parcelable 래퍼

Parcelable 래퍼는 SDK가 수정할 수 없는 비 Parcelable 유형에 대한 지원을 추가하는 방법입니다. SDK에는 여러 유형의 래퍼가 포함되어 있지만, 사용해야 하는 유형이 포함되어 있지 않으면 직접 작성해야 합니다.

Parcelable 래퍼는 다른 클래스를 래핑하고 Parcelable로 만들도록 설계된 클래스입니다. 정의된 정적 계약을 따르며 SDK에 등록되므로 지정된 유형을 parcelable 유형으로 변환하고 parcelable 유형에서 해당 유형을 추출하는 데 사용할 수 있습니다.

Annotation

parcelable 래퍼 클래스는 @CustomParcelableWrapper 주석을 달고 래핑된 클래스를 originalType로 지정해야 합니다. 예를 들면 다음과 같습니다.

@CustomParcelableWrapper(originalType=ImmutableList.class)

형식

Parcelable 래퍼는 Parcelable를 올바르게 구현해야 하며 래핑된 유형을 래핑하는 정적 W of(Bundler, BundlerType, T) 메서드와 래핑된 유형을 반환하는 비정적 T get() 메서드가 있어야 합니다.

SDK는 이러한 메서드를 사용하여 유형을 원활하게 지원합니다.

Bundler

제네릭 유형 (예: 목록 및 맵)을 래핑할 수 있도록 of 메서드에는 지원되는 모든 유형을 Parcel에 읽기 (#readFromParcel 사용) 및 쓰기 (#writeToParcel 사용)할 수 있는 Bundler와 쓰려는 선언된 유형을 나타내는 BundlerType가 전달됩니다.

BundlerBundlerType 인스턴스는 자체적으로 parcelable이며 parcelable 래퍼를 재구성할 때 사용할 수 있도록 parcelable 래퍼의 parceling의 일부로 작성해야 합니다.

BundlerType가 제네릭 유형을 나타내는 경우 .typeArguments()를 호출하여 유형 변수를 찾을 수 있습니다. 각 유형 인수는 자체적으로 BundlerType입니다.

예를 보려면 ParcelableCustomWrapper를 참고하세요.

public class CustomWrapper<F> {
  private final F value;

  public CustomWrapper(F value) {
    this.value = value;
  }
  public F value() {
    return value;
  }
}

@CustomParcelableWrapper(originalType = CustomWrapper.class)
public class ParcelableCustomWrapper<E> implements Parcelable {

  private static final int NULL = -1;
  private static final int NOT_NULL = 1;

  private final Bundler bundler;
  private final BundlerType type;
  private final CustomWrapper<E> customWrapper;

  /**
  *   Create a wrapper for a given {@link CustomWrapper}.
  *
  *   <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
  */
  public static <F> ParcelableCustomWrapper<F> of(
      Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {
    return new ParcelableCustomWrapper<>(bundler, type, customWrapper);
  }

  public CustomWrapper<E> get() {
    return customWrapper;
  }

  private ParcelableCustomWrapper(
      Bundler bundler, BundlerType type, CustomWrapper<E> customWrapper) {
    if (bundler == null || type == null) {
      throw new NullPointerException();
    }
    this.bundler = bundler;
    this.type = type;
    this.customWrapper = customWrapper;
  }

  private ParcelableCustomWrapper(Parcel in) {
    bundler = in.readParcelable(Bundler.class.getClassLoader());

    int presentValue = in.readInt();

    if (presentValue == NULL) {
      type = null;
      customWrapper = null;
      return;
    }

    type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
    BundlerType valueType = type.typeArguments().get(0);

    @SuppressWarnings("unchecked")
    E value = (E) bundler.readFromParcel(in, valueType);

    customWrapper = new CustomWrapper<>(value);
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeParcelable(bundler, flags);

    if (customWrapper == null) {
      dest.writeInt(NULL);
      return;
    }

    dest.writeInt(NOT_NULL);
    dest.writeParcelable(type, flags);
    BundlerType valueType = type.typeArguments().get(0);
    bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);
  }

  @Override
  public int describeContents() {
    return 0;
  }

  @SuppressWarnings("rawtypes")
  public static final Creator<ParcelableCustomWrapper> CREATOR =
    new Creator<ParcelableCustomWrapper>() {
      @Override
      public ParcelableCustomWrapper createFromParcel(Parcel in) {
        return new ParcelableCustomWrapper(in);
      }

      @Override
      public ParcelableCustomWrapper[] newArray(int size) {
        return new ParcelableCustomWrapper[size];
      }
    };
}

SDK에 등록

맞춤 parcelable 래퍼를 만들었다면 SDK에 등록해야 사용할 수 있습니다.

이렇게 하려면 클래스의 CustomProfileConnector 주석 또는 CrossProfile 주석에 parcelableWrappers={YourParcelableWrapper.class}를 지정합니다.

Future 래퍼

Future Wrapper는 SDK가 프로필 전반에서 future에 대한 지원을 추가하는 방법입니다. SDK에는 기본적으로 ListenableFuture 지원이 포함되어 있지만 다른 Future 유형의 경우 직접 지원을 추가할 수 있습니다.

Future 래퍼는 특정 Future 유형을 래핑하고 SDK에서 사용할 수 있도록 설계된 클래스입니다. 정의된 정적 계약을 따르며 SDK에 등록해야 합니다.

Annotation

향후 래퍼 클래스는 @CustomFutureWrapper 주석을 달아 래핑된 클래스를 originalType로 지정해야 합니다. 예를 들면 다음과 같습니다.

@CustomFutureWrapper(originalType=SettableFuture.class)

형식

향후 래퍼는 com.google.android.enterprise.connectedapps.FutureWrapper를 확장해야 합니다.

향후 래퍼에는 래퍼의 인스턴스를 만드는 정적 W create(Bundler, BundlerType) 메서드가 있어야 합니다. 동시에 래핑된 future 유형의 인스턴스가 만들어집니다. 이는 비정적 T getFuture() 메서드에서 반환해야 합니다. onResult(E)onException(Throwable) 메서드는 결과 또는 throwable을 래핑된 future에 전달하도록 구현해야 합니다.

향후 래퍼에는 정적 void writeFutureResult(Bundler, BundlerType, T, FutureResultWriter<E>) 메서드도 있어야 합니다. 이는 결과를 위해 future에 전달된 값에 등록되어야 하며, 결과가 주어지면 resultWriter.onSuccess(value)를 호출합니다. 예외가 발생하면 resultWriter.onFailure(exception)를 호출해야 합니다.

마지막으로 future 래퍼에는 프로필에서 future로의 맵을 프로필에서 결과로의 맵의 future로 변환하는 정적 T<Map<Profile, E>> groupResults(Map<Profile, T<E>> results) 메서드도 있어야 합니다. CrossProfileCallbackMultiMerger를 사용하면 이 로직을 더 쉽게 만들 수 있습니다.

예를 들면 다음과 같습니다.

/** A basic implementation of the future pattern used to test custom future
wrappers. */
public class SimpleFuture<E> {
  public static interface Consumer<E> {
    void accept(E value);
  }
  private E value;
  private Throwable thrown;
  private final CountDownLatch countDownLatch = new CountDownLatch(1);
  private Consumer<E> callback;
  private Consumer<Throwable> exceptionCallback;

  public void set(E value) {
    this.value = value;
    countDownLatch.countDown();
    if (callback != null) {
      callback.accept(value);
    }
  }

  public void setException(Throwable t) {
    this.thrown = t;
    countDownLatch.countDown();
    if (exceptionCallback != null) {
      exceptionCallback.accept(thrown);
    }
  }

  public E get() {
    try {
      countDownLatch.await();
    } catch (InterruptedException e) {
      eturn null;
    }
    if (thrown != null) {
      throw new RuntimeException(thrown);
    }
    return value;
  }

  public void setCallback(Consumer<E> callback, Consumer<Throwable>
exceptionCallback) {
    if (value != null) {
      callback.accept(value);
    } else if (thrown != null) {
      exceptionCallback.accept(thrown);
    } else {
      this.callback = callback;
      this.exceptionCallback = exceptionCallback;
    }
  }
}
/** Wrapper for adding support for {@link SimpleFuture} to the Connected Apps SDK.
*/
@CustomFutureWrapper(originalType = SimpleFuture.class)
public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {

  private final SimpleFuture<E> future = new SimpleFuture<>();

  public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType
bundlerType) {
    return new SimpleFutureWrapper<>(bundler, bundlerType);
  }

  private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {
    super(bundler, bundlerType);
  }

  public SimpleFuture<E> getFuture() {
    return future;
  }

  @Override
  public void onResult(E result) {
    future.set(result);
  }

  @Override
  public void onException(Throwable throwable) {
    future.setException(throwable);
  }

  public static <E> void writeFutureResult(
      SimpleFuture<E> future, FutureResultWriter<E> resultWriter) {

    future.setCallback(resultWriter::onSuccess, resultWriter::onFailure);
  }

  public static <E> SimpleFuture<Map<Profile, E>> groupResults(
      Map<Profile, SimpleFuture<E>> results) {
    SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();

    CrossProfileCallbackMultiMerger<E> merger =
        new CrossProfileCallbackMultiMerger<>(results.size(), m::set);
    for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {
      result
        .getValue()
        .setCallback(
          (value) -> merger.onResult(result.getKey(), value),
          (throwable) -> merger.missingResult(result.getKey()));
    }
    return m;
  }
}

SDK에 등록

맞춤 future 래퍼를 만들었다면 SDK에 등록해야 사용할 수 있습니다.

이렇게 하려면 클래스의 CustomProfileConnector 주석 또는 CrossProfile 주석에 futureWrappers={YourFutureWrapper.class}를 지정합니다.

직접 부팅 모드

앱에서 직접 부팅 모드를 지원하는 경우 프로필이 잠금 해제되기 전에 교차 프로필 호출을 실행해야 할 수 있습니다. 기본적으로 SDK는 다른 프로필이 잠금 해제된 경우에만 연결을 허용합니다.

이 동작을 변경하려면 맞춤 프로필 커넥터를 사용하는 경우 availabilityRestrictions=AvailabilityRestrictions.DIRECT_BOOT_AWARE를 지정해야 합니다.

@GeneratedProfileConnector
@CustomProfileConnector(availabilityRestrictions=AvailabilityRestrictions.DIRECT_BO
OT_AWARE)
public interface MyProfileConnector extends ProfileConnector {
  public static MyProfileConnector create(Context context) {
    return GeneratedMyProfileConnector.builder(context).build();
  }
}

CrossProfileConnector를 사용하는 경우 빌더에서 .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT _AWARE를 사용합니다.

이번 변경으로 다른 프로필이 잠금 해제되지 않은 경우 사용 가능 여부가 표시되고 교차 프로필 호출을 할 수 있습니다. 호출이 기기 암호화 저장소에만 액세스하도록 하는 것은 개발자의 책임입니다.