1 /* 2 * Copyright (C) 2017 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 17 package com.android.companiondevicemanager; 18 19 import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; 20 import static android.text.TextUtils.emptyIfNull; 21 import static android.text.TextUtils.isEmpty; 22 import static android.text.TextUtils.withoutPrefix; 23 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 24 25 import static java.util.Objects.requireNonNull; 26 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.app.Activity; 30 import android.companion.AssociationRequest; 31 import android.companion.CompanionDeviceManager; 32 import android.content.Intent; 33 import android.content.pm.PackageManager; 34 import android.content.res.Resources; 35 import android.content.res.TypedArray; 36 import android.database.DataSetObserver; 37 import android.graphics.Color; 38 import android.graphics.drawable.Drawable; 39 import android.os.Bundle; 40 import android.text.Html; 41 import android.util.Log; 42 import android.util.SparseArray; 43 import android.util.TypedValue; 44 import android.view.Gravity; 45 import android.view.View; 46 import android.view.ViewGroup; 47 import android.widget.BaseAdapter; 48 import android.widget.ListView; 49 import android.widget.ProgressBar; 50 import android.widget.TextView; 51 52 import com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DeviceFilterPair; 53 import com.android.internal.util.Preconditions; 54 55 public class CompanionDeviceActivity extends Activity { 56 57 private static final boolean DEBUG = false; 58 private static final String LOG_TAG = CompanionDeviceActivity.class.getSimpleName(); 59 60 static CompanionDeviceActivity sInstance; 61 62 View mLoadingIndicator = null; 63 ListView mDeviceListView; 64 private View mPairButton; 65 private View mCancelButton; 66 67 DevicesAdapter mDevicesAdapter; 68 69 @Override onCreate(Bundle savedInstanceState)70 public void onCreate(Bundle savedInstanceState) { 71 super.onCreate(savedInstanceState); 72 73 Log.i(LOG_TAG, "Starting UI for " + getService().mRequest); 74 75 if (getService().mDevicesFound.isEmpty()) { 76 Log.e(LOG_TAG, "About to show UI, but no devices to show"); 77 } 78 79 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 80 sInstance = this; 81 getService().mActivity = this; 82 83 String deviceProfile = getRequest().getDeviceProfile(); 84 String profilePrivacyDisclaimer = emptyIfNull(getRequest() 85 .getDeviceProfilePrivilegesDescription()) 86 .replace("APP_NAME", getCallingAppName()); 87 boolean useDeviceProfile = deviceProfile != null && !isEmpty(profilePrivacyDisclaimer); 88 String profileName = useDeviceProfile 89 ? getDeviceProfileName(deviceProfile) 90 : getString(R.string.profile_name_generic); 91 92 if (getRequest().isSingleDevice()) { 93 setContentView(R.layout.device_confirmation); 94 final DeviceFilterPair selectedDevice = getService().mDevicesFound.get(0); 95 setTitle(Html.fromHtml(getString( 96 R.string.confirmation_title, 97 Html.escapeHtml(getCallingAppName()), 98 Html.escapeHtml(selectedDevice.getDisplayName())), 0)); 99 100 mPairButton = findViewById(R.id.button_pair); 101 mPairButton.setOnClickListener(v -> onDeviceConfirmed(getService().mSelectedDevice)); 102 getService().mSelectedDevice = selectedDevice; 103 onSelectionUpdate(); 104 if (getRequest().isSkipPrompt()) { 105 onDeviceConfirmed(selectedDevice); 106 } 107 } else { 108 setContentView(R.layout.device_chooser); 109 mPairButton = findViewById(R.id.button_pair); 110 mPairButton.setVisibility(View.GONE); 111 setTitle(Html.fromHtml(getString(R.string.chooser_title, 112 Html.escapeHtml(profileName), 113 Html.escapeHtml(getCallingAppName())), 0)); 114 mDeviceListView = findViewById(R.id.device_list); 115 mDevicesAdapter = new DevicesAdapter(); 116 mDeviceListView.setAdapter(mDevicesAdapter); 117 mDeviceListView.setOnItemClickListener((adapterView, view, pos, l) -> { 118 getService().mSelectedDevice = 119 (DeviceFilterPair) adapterView.getItemAtPosition(pos); 120 mDevicesAdapter.notifyDataSetChanged(); 121 }); 122 mDevicesAdapter.registerDataSetObserver(new DataSetObserver() { 123 @Override 124 public void onChanged() { 125 onSelectionUpdate(); 126 } 127 }); 128 mDeviceListView.addFooterView(mLoadingIndicator = getProgressBar(), null, false); 129 } 130 131 TextView profileSummary = findViewById(R.id.profile_summary); 132 133 if (useDeviceProfile) { 134 profileSummary.setVisibility(View.VISIBLE); 135 String deviceRef = getRequest().isSingleDevice() 136 ? getService().mDevicesFound.get(0).getDisplayName() 137 : profileName; 138 profileSummary.setText(getString(R.string.profile_summary, 139 deviceRef, 140 profilePrivacyDisclaimer)); 141 } else { 142 profileSummary.setVisibility(View.GONE); 143 } 144 145 mCancelButton = findViewById(R.id.button_cancel); 146 mCancelButton.setOnClickListener(v -> cancel()); 147 } 148 notifyDevicesChanged()149 static void notifyDevicesChanged() { 150 if (sInstance != null && sInstance.mDevicesAdapter != null && !sInstance.isFinishing()) { 151 sInstance.mDevicesAdapter.notifyDataSetChanged(); 152 } 153 } 154 getRequest()155 private AssociationRequest getRequest() { 156 return getService().mRequest; 157 } 158 getDeviceProfileName(@ullable String deviceProfile)159 private String getDeviceProfileName(@Nullable String deviceProfile) { 160 if (deviceProfile == null) { 161 return getString(R.string.profile_name_generic); 162 } 163 switch (deviceProfile) { 164 case AssociationRequest.DEVICE_PROFILE_WATCH: { 165 return getString(R.string.profile_name_watch); 166 } 167 default: { 168 Log.w(LOG_TAG, 169 "No localized profile name found for device profile: " + deviceProfile); 170 return withoutPrefix("android.app.role.COMPANION_DEVICE_", deviceProfile) 171 .toLowerCase() 172 .replace('_', ' '); 173 } 174 } 175 } 176 cancel()177 private void cancel() { 178 Log.i(LOG_TAG, "cancel()"); 179 getService().onCancel(); 180 setResult(RESULT_CANCELED); 181 finish(); 182 } 183 184 @Override onStop()185 protected void onStop() { 186 super.onStop(); 187 if (!isFinishing() && !isChangingConfigurations()) { 188 Log.i(LOG_TAG, "onStop() - cancelling"); 189 cancel(); 190 } 191 } 192 193 @Override onDestroy()194 protected void onDestroy() { 195 super.onDestroy(); 196 getService().mActivity = null; 197 if (sInstance == this) { 198 sInstance = null; 199 } 200 } 201 getCallingAppName()202 private CharSequence getCallingAppName() { 203 try { 204 final PackageManager packageManager = getPackageManager(); 205 String callingPackage = Preconditions.checkStringNotEmpty( 206 getCallingPackage(), 207 "This activity must be called for result"); 208 return packageManager.getApplicationLabel( 209 packageManager.getApplicationInfo(callingPackage, 0)); 210 } catch (PackageManager.NameNotFoundException e) { 211 throw new RuntimeException(e); 212 } 213 } 214 215 @Override getCallingPackage()216 public String getCallingPackage() { 217 return requireNonNull(getRequest().getCallingPackage()); 218 } 219 220 @Override setTitle(CharSequence title)221 public void setTitle(CharSequence title) { 222 final TextView titleView = findViewById(R.id.title); 223 final int padding = getPadding(getResources()); 224 titleView.setPadding(padding, padding, padding, padding); 225 titleView.setText(title); 226 } 227 getProgressBar()228 private ProgressBar getProgressBar() { 229 final ProgressBar progressBar = new ProgressBar(this); 230 progressBar.setForegroundGravity(Gravity.CENTER_HORIZONTAL); 231 final int padding = getPadding(getResources()); 232 progressBar.setPadding(padding, padding, padding, padding); 233 return progressBar; 234 } 235 getPadding(Resources r)236 static int getPadding(Resources r) { 237 return r.getDimensionPixelSize(R.dimen.padding); 238 } 239 onSelectionUpdate()240 private void onSelectionUpdate() { 241 DeviceFilterPair selectedDevice = getService().mSelectedDevice; 242 if (mPairButton.getVisibility() != View.VISIBLE && selectedDevice != null) { 243 onDeviceConfirmed(selectedDevice); 244 } else { 245 mPairButton.setEnabled(selectedDevice != null); 246 } 247 } 248 getService()249 private CompanionDeviceDiscoveryService getService() { 250 return CompanionDeviceDiscoveryService.sInstance; 251 } 252 onDeviceConfirmed(DeviceFilterPair selectedDevice)253 protected void onDeviceConfirmed(DeviceFilterPair selectedDevice) { 254 Log.i(LOG_TAG, "onDeviceConfirmed(selectedDevice = " + selectedDevice + ")"); 255 getService().onDeviceSelected( 256 getCallingPackage(), getDeviceMacAddress(selectedDevice.device)); 257 } 258 setResultAndFinish()259 void setResultAndFinish() { 260 Log.i(LOG_TAG, "setResultAndFinish(selectedDevice = " 261 + getService().mSelectedDevice.device + ")"); 262 setResult(RESULT_OK, 263 new Intent().putExtra( 264 CompanionDeviceManager.EXTRA_DEVICE, getService().mSelectedDevice.device)); 265 finish(); 266 } 267 268 class DevicesAdapter extends BaseAdapter { 269 private final Drawable mBluetoothIcon = icon(android.R.drawable.stat_sys_data_bluetooth); 270 private final Drawable mWifiIcon = icon(com.android.internal.R.drawable.ic_wifi_signal_3); 271 272 private SparseArray<Integer> mColors = new SparseArray(); 273 icon(int drawableRes)274 private Drawable icon(int drawableRes) { 275 Drawable icon = getResources().getDrawable(drawableRes, null); 276 icon.setTint(Color.DKGRAY); 277 return icon; 278 } 279 280 @Override getView( int position, @Nullable View convertView, @NonNull ViewGroup parent)281 public View getView( 282 int position, 283 @Nullable View convertView, 284 @NonNull ViewGroup parent) { 285 TextView view = convertView instanceof TextView 286 ? (TextView) convertView 287 : newView(); 288 bind(view, getItem(position)); 289 return view; 290 } 291 bind(TextView textView, DeviceFilterPair device)292 private void bind(TextView textView, DeviceFilterPair device) { 293 textView.setText(device.getDisplayName()); 294 textView.setBackgroundColor( 295 device.equals(getService().mSelectedDevice) 296 ? getColor(android.R.attr.colorControlHighlight) 297 : Color.TRANSPARENT); 298 textView.setCompoundDrawablesWithIntrinsicBounds( 299 device.device instanceof android.net.wifi.ScanResult 300 ? mWifiIcon 301 : mBluetoothIcon, 302 null, null, null); 303 textView.getCompoundDrawables()[0].setTint(getColor(android.R.attr.colorForeground)); 304 } 305 newView()306 private TextView newView() { 307 final TextView textView = new TextView(CompanionDeviceActivity.this); 308 textView.setTextColor(getColor(android.R.attr.colorForeground)); 309 final int padding = CompanionDeviceActivity.getPadding(getResources()); 310 textView.setPadding(padding, padding, padding, padding); 311 textView.setCompoundDrawablePadding(padding); 312 return textView; 313 } 314 getColor(int colorAttr)315 private int getColor(int colorAttr) { 316 if (mColors.contains(colorAttr)) { 317 return mColors.get(colorAttr); 318 } 319 TypedValue typedValue = new TypedValue(); 320 TypedArray a = obtainStyledAttributes(typedValue.data, new int[] { colorAttr }); 321 int result = a.getColor(0, 0); 322 a.recycle(); 323 mColors.put(colorAttr, result); 324 return result; 325 } 326 327 @Override getCount()328 public int getCount() { 329 return getService().mDevicesFound.size(); 330 } 331 332 @Override getItem(int position)333 public DeviceFilterPair getItem(int position) { 334 return getService().mDevicesFound.get(position); 335 } 336 337 @Override getItemId(int position)338 public long getItemId(int position) { 339 return position; 340 } 341 } 342 } 343