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