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 82 String deviceProfile = getRequest().getDeviceProfile(); 83 String profilePrivacyDisclaimer = emptyIfNull(getRequest() 84 .getDeviceProfilePrivilegesDescription()) 85 .replace("APP_NAME", getCallingAppName()); 86 boolean useDeviceProfile = deviceProfile != null && !isEmpty(profilePrivacyDisclaimer); 87 String profileName = useDeviceProfile 88 ? getDeviceProfileName(deviceProfile) 89 : getString(R.string.profile_name_generic); 90 91 if (getRequest().isSingleDevice()) { 92 setContentView(R.layout.device_confirmation); 93 final DeviceFilterPair selectedDevice = getService().mDevicesFound.get(0); 94 setTitle(Html.fromHtml(getString( 95 R.string.confirmation_title, 96 getCallingAppName(), 97 profileName, 98 selectedDevice.getDisplayName()), 0)); 99 mPairButton = findViewById(R.id.button_pair); 100 mPairButton.setOnClickListener(v -> onDeviceConfirmed(getService().mSelectedDevice)); 101 getService().mSelectedDevice = selectedDevice; 102 onSelectionUpdate(); 103 if (getRequest().isSkipPrompt()) { 104 onDeviceConfirmed(selectedDevice); 105 } 106 } else { 107 setContentView(R.layout.device_chooser); 108 mPairButton = findViewById(R.id.button_pair); 109 mPairButton.setVisibility(View.GONE); 110 setTitle(Html.fromHtml(getString(R.string.chooser_title, 111 profileName, 112 getCallingAppName()), 0)); 113 mDeviceListView = findViewById(R.id.device_list); 114 mDevicesAdapter = new DevicesAdapter(); 115 mDeviceListView.setAdapter(mDevicesAdapter); 116 mDeviceListView.setOnItemClickListener((adapterView, view, pos, l) -> { 117 getService().mSelectedDevice = 118 (DeviceFilterPair) adapterView.getItemAtPosition(pos); 119 mDevicesAdapter.notifyDataSetChanged(); 120 }); 121 mDevicesAdapter.registerDataSetObserver(new DataSetObserver() { 122 @Override 123 public void onChanged() { 124 onSelectionUpdate(); 125 } 126 }); 127 mDeviceListView.addFooterView(mLoadingIndicator = getProgressBar(), null, false); 128 } 129 130 TextView profileSummary = findViewById(R.id.profile_summary); 131 132 if (useDeviceProfile) { 133 profileSummary.setVisibility(View.VISIBLE); 134 String deviceRef = getRequest().isSingleDevice() 135 ? getService().mDevicesFound.get(0).getDisplayName() 136 : profileName; 137 profileSummary.setText(getString(R.string.profile_summary, 138 deviceRef, 139 profilePrivacyDisclaimer)); 140 } else { 141 profileSummary.setVisibility(View.GONE); 142 } 143 144 getService().mActivity = this; 145 146 mCancelButton = findViewById(R.id.button_cancel); 147 mCancelButton.setOnClickListener(v -> cancel()); 148 } 149 notifyDevicesChanged()150 static void notifyDevicesChanged() { 151 if (sInstance != null && sInstance.mDevicesAdapter != null && !sInstance.isFinishing()) { 152 sInstance.mDevicesAdapter.notifyDataSetChanged(); 153 } 154 } 155 getRequest()156 private AssociationRequest getRequest() { 157 return getService().mRequest; 158 } 159 getDeviceProfileName(@ullable String deviceProfile)160 private String getDeviceProfileName(@Nullable String deviceProfile) { 161 if (deviceProfile == null) { 162 return getString(R.string.profile_name_generic); 163 } 164 switch (deviceProfile) { 165 case AssociationRequest.DEVICE_PROFILE_WATCH: { 166 return getString(R.string.profile_name_watch); 167 } 168 default: { 169 Log.w(LOG_TAG, 170 "No localized profile name found for device profile: " + deviceProfile); 171 return withoutPrefix("android.app.role.COMPANION_DEVICE_", deviceProfile) 172 .toLowerCase() 173 .replace('_', ' '); 174 } 175 } 176 } 177 cancel()178 private void cancel() { 179 Log.i(LOG_TAG, "cancel()"); 180 getService().onCancel(); 181 setResult(RESULT_CANCELED); 182 finish(); 183 } 184 185 @Override onStop()186 protected void onStop() { 187 super.onStop(); 188 if (!isFinishing() && !isChangingConfigurations()) { 189 Log.i(LOG_TAG, "onStop() - cancelling"); 190 cancel(); 191 } 192 } 193 194 @Override onDestroy()195 protected void onDestroy() { 196 super.onDestroy(); 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 setResult(RESULT_OK, 258 new Intent().putExtra(CompanionDeviceManager.EXTRA_DEVICE, selectedDevice.device)); 259 finish(); 260 } 261 262 class DevicesAdapter extends BaseAdapter { 263 private final Drawable mBluetoothIcon = icon(android.R.drawable.stat_sys_data_bluetooth); 264 private final Drawable mWifiIcon = icon(com.android.internal.R.drawable.ic_wifi_signal_3); 265 266 private SparseArray<Integer> mColors = new SparseArray(); 267 icon(int drawableRes)268 private Drawable icon(int drawableRes) { 269 Drawable icon = getResources().getDrawable(drawableRes, null); 270 icon.setTint(Color.DKGRAY); 271 return icon; 272 } 273 274 @Override getView( int position, @Nullable View convertView, @NonNull ViewGroup parent)275 public View getView( 276 int position, 277 @Nullable View convertView, 278 @NonNull ViewGroup parent) { 279 TextView view = convertView instanceof TextView 280 ? (TextView) convertView 281 : newView(); 282 bind(view, getItem(position)); 283 return view; 284 } 285 bind(TextView textView, DeviceFilterPair device)286 private void bind(TextView textView, DeviceFilterPair device) { 287 textView.setText(device.getDisplayName()); 288 textView.setBackgroundColor( 289 device.equals(getService().mSelectedDevice) 290 ? getColor(android.R.attr.colorControlHighlight) 291 : Color.TRANSPARENT); 292 textView.setCompoundDrawablesWithIntrinsicBounds( 293 device.device instanceof android.net.wifi.ScanResult 294 ? mWifiIcon 295 : mBluetoothIcon, 296 null, null, null); 297 textView.getCompoundDrawables()[0].setTint(getColor(android.R.attr.colorForeground)); 298 } 299 newView()300 private TextView newView() { 301 final TextView textView = new TextView(CompanionDeviceActivity.this); 302 textView.setTextColor(getColor(android.R.attr.colorForeground)); 303 final int padding = CompanionDeviceActivity.getPadding(getResources()); 304 textView.setPadding(padding, padding, padding, padding); 305 textView.setCompoundDrawablePadding(padding); 306 return textView; 307 } 308 getColor(int colorAttr)309 private int getColor(int colorAttr) { 310 if (mColors.contains(colorAttr)) { 311 return mColors.get(colorAttr); 312 } 313 TypedValue typedValue = new TypedValue(); 314 TypedArray a = obtainStyledAttributes(typedValue.data, new int[] { colorAttr }); 315 int result = a.getColor(0, 0); 316 a.recycle(); 317 mColors.put(colorAttr, result); 318 return result; 319 } 320 321 @Override getCount()322 public int getCount() { 323 return getService().mDevicesFound.size(); 324 } 325 326 @Override getItem(int position)327 public DeviceFilterPair getItem(int position) { 328 return getService().mDevicesFound.get(position); 329 } 330 331 @Override getItemId(int position)332 public long getItemId(int position) { 333 return position; 334 } 335 } 336 } 337