1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.example.adservices.samples.topics.sampleapp1; 17 18 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 19 20 import android.adservices.clients.topics.AdvertisingTopicsClient; 21 import android.adservices.topics.EncryptedTopic; 22 import android.adservices.topics.GetTopicsResponse; 23 import android.adservices.topics.Topic; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.util.Log; 29 import android.view.View; 30 import android.widget.Button; 31 import android.widget.TextView; 32 33 import androidx.appcompat.app.AppCompatActivity; 34 35 import com.android.adservices.HpkeJni; 36 37 import com.google.common.primitives.Bytes; 38 import com.google.common.util.concurrent.FutureCallback; 39 import com.google.common.util.concurrent.Futures; 40 import com.google.common.util.concurrent.ListenableFuture; 41 42 import java.io.PrintWriter; 43 import java.io.StringWriter; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import java.util.Base64; 47 import java.util.List; 48 import java.util.concurrent.Executor; 49 import java.util.concurrent.Executors; 50 51 /** 52 * Android application activity for testing Topics API by providing a button in UI that initiate 53 * user's interaction with Topics Manager in the background. Response from Topics API will be shown 54 * in the app as text as well as toast message. In case anything goes wrong in this process, error 55 * message will also be shown in toast to suggest the Exception encountered. 56 */ 57 public class MainActivity extends AppCompatActivity { 58 59 private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool(); 60 private static final String NEWLINE = "\n"; 61 private static final String TAG = "SampleApp1"; 62 private static final String RB_SETTING_APP_INTENT = "android.adservices.ui.SETTINGS"; 63 private static final List<String> SDK_NAMES = new ArrayList<>(Arrays.asList("SdkName1")); 64 // Test constants for testing encryption 65 private static final String TEST_PRIVATE_KEY_BASE64 = 66 "f86EzLmGaVmc+PwjJk5ADPE4ijQvliWf0CQyY/Zyy7I="; 67 private static final byte[] DECODED_PRIVATE_KEY = 68 Base64.getDecoder().decode(TEST_PRIVATE_KEY_BASE64); 69 private static final byte[] EMPTY_CONTEXT_INFO = new byte[] {}; 70 private Button mTopicsClientButton; 71 private Button mTopicsPreviewButton; 72 private TextView mResultTextView; 73 private Button mSettingsAppButton; 74 private AdvertisingTopicsClient mAdvertisingTopicsClient; 75 private Handler mHandler; 76 77 @Override onCreate(Bundle savedInstanceState)78 protected void onCreate(Bundle savedInstanceState) { 79 super.onCreate(savedInstanceState); 80 setContentView(R.layout.activity_main); 81 mTopicsClientButton = findViewById(R.id.topics_client_button); 82 mSettingsAppButton = findViewById(R.id.settings_app_launch_button); 83 mTopicsPreviewButton = findViewById(R.id.topics_preview_button); 84 mResultTextView = findViewById(R.id.textView); 85 registerGetTopicsButton(); 86 registerLauchSettingsAppButton(); 87 registerTopicsPreviewButton(); 88 mHandler = new Handler(); 89 } 90 registerGetTopicsButton()91 private void registerGetTopicsButton() { 92 mTopicsClientButton.setOnClickListener( 93 v -> { 94 mResultTextView.setText(""); 95 mResultTextView.append("Topics -> "); 96 for (String sdkName : SDK_NAMES) { 97 getTopics(sdkName, true); 98 } 99 }); 100 } 101 registerTopicsPreviewButton()102 private void registerTopicsPreviewButton() { 103 mTopicsPreviewButton.setOnClickListener( 104 v -> { 105 mResultTextView.setText(""); 106 mResultTextView.append("Preview Topics -> "); 107 for (String sdkName : SDK_NAMES) { 108 getTopics(sdkName, false); 109 } 110 }); 111 } 112 getTopics(List<Topic> arr)113 private String getTopics(List<Topic> arr) { 114 StringBuilder sb = new StringBuilder(); 115 int index = 1; 116 for (Topic topic : arr) { 117 sb.append(index++).append(". ").append(topic.toString()).append(NEWLINE); 118 } 119 return sb.toString(); 120 } 121 getDecryptedTopics(List<EncryptedTopic> arr)122 private String getDecryptedTopics(List<EncryptedTopic> arr) { 123 StringBuilder sb = new StringBuilder(); 124 int index = 1; 125 for (EncryptedTopic encryptedTopic : arr) { 126 byte[] cipherText = 127 Bytes.concat( 128 encryptedTopic.getEncapsulatedKey(), 129 encryptedTopic.getEncryptedTopic()); 130 byte[] decryptedText = 131 HpkeJni.decrypt(DECODED_PRIVATE_KEY, cipherText, EMPTY_CONTEXT_INFO); 132 sb.append(index++).append(". ").append(new String(decryptedText)).append(NEWLINE); 133 } 134 return sb.toString(); 135 } 136 137 @SuppressWarnings("NewApi") getTopics(String sdkName, boolean shouldRecordObservation)138 private void getTopics(String sdkName, boolean shouldRecordObservation) { 139 // On R, Privacy Sandbox is initially disabled. 140 try { 141 mAdvertisingTopicsClient = 142 new AdvertisingTopicsClient.Builder() 143 .setContext(this) 144 .setSdkName(sdkName) 145 .setExecutor(CALLBACK_EXECUTOR) 146 .setShouldRecordObservation(shouldRecordObservation) 147 .build(); 148 } catch (IllegalStateException e) { 149 mHandler.post( 150 new Runnable() { 151 @Override 152 public void run() { 153 mResultTextView.append("Privacy Sandbox is not available."); 154 } 155 }); 156 return; 157 } 158 ListenableFuture<GetTopicsResponse> getTopicsResponseFuture = 159 mAdvertisingTopicsClient.getTopics(); 160 161 Futures.addCallback( 162 getTopicsResponseFuture, 163 new FutureCallback<GetTopicsResponse>() { 164 @Override 165 public void onSuccess(GetTopicsResponse result) { 166 Log.d(TAG, "GetTopics for sdk " + sdkName + " succeeded!"); 167 String topics = getTopics(result.getTopics()); 168 String encryptedTopicsDecrypted = 169 getDecryptedTopics(result.getEncryptedTopics()); 170 171 mHandler.post( 172 new Runnable() { 173 @Override 174 public void run() { 175 mResultTextView.append( 176 sdkName 177 + "'s topics: " 178 + NEWLINE 179 + topics 180 + NEWLINE); 181 mResultTextView.append( 182 sdkName 183 + "'s encrypted topics, decrypted: " 184 + NEWLINE 185 + encryptedTopicsDecrypted 186 + NEWLINE); 187 } 188 }); 189 Log.d(TAG, sdkName + "'s topics: " + NEWLINE + topics + NEWLINE); 190 Log.d( 191 TAG, 192 sdkName 193 + "'s encrypted topics, decrypted: " 194 + NEWLINE 195 + encryptedTopicsDecrypted 196 + NEWLINE); 197 } 198 199 @Override 200 public void onFailure(Throwable t) { 201 StringWriter sw = new StringWriter(); 202 PrintWriter pw = new PrintWriter(sw); 203 t.printStackTrace(pw); 204 Log.e( 205 TAG, 206 "Failed to getTopics for sdk " + sdkName + ": " + t.getMessage()); 207 mHandler.post( 208 new Runnable() { 209 @Override 210 public void run() { 211 mResultTextView.append( 212 "Failed to getTopics for sdk " 213 + sdkName 214 + ": " 215 + t.toString() 216 + NEWLINE); 217 } 218 }); 219 } 220 }, 221 directExecutor()); 222 } 223 registerLauchSettingsAppButton()224 private void registerLauchSettingsAppButton() { 225 mSettingsAppButton.setOnClickListener( 226 new View.OnClickListener() { 227 228 @Override 229 public void onClick(View view) { 230 // Use try-catch to handle open Settings App failure and show 231 // error msg in Sample App 232 try { 233 Context context = getApplicationContext(); 234 Intent activity2Intent = new Intent(RB_SETTING_APP_INTENT); 235 activity2Intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 236 context.startActivity(activity2Intent); 237 } catch (Exception err) { 238 mHandler.post( 239 new Runnable() { 240 @Override 241 public void run() { 242 mResultTextView.append( 243 "Failed to open SETTINGS APP: " 244 + err.getMessage() 245 + NEWLINE); 246 } 247 }); 248 } 249 } 250 }); 251 } 252 } 253