1 /*
2  * Copyright 2023 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.example.androidx.mediarouting.activities.systemrouting;
18 
19 import static android.Manifest.permission.BLUETOOTH_CONNECT;
20 import static android.Manifest.permission.BLUETOOTH_SCAN;
21 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
22 
23 import android.content.Context;
24 import android.content.Intent;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.widget.Toast;
28 
29 import androidx.appcompat.app.AppCompatActivity;
30 import androidx.core.app.ActivityCompat;
31 import androidx.core.content.ContextCompat;
32 import androidx.recyclerview.widget.LinearLayoutManager;
33 import androidx.recyclerview.widget.RecyclerView;
34 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
35 
36 import com.example.androidx.mediarouting.R;
37 import com.example.androidx.mediarouting.activities.systemrouting.source.AndroidXMediaRouterSystemRoutesSource;
38 import com.example.androidx.mediarouting.activities.systemrouting.source.AudioManagerSystemRoutesSource;
39 import com.example.androidx.mediarouting.activities.systemrouting.source.BluetoothManagerSystemRoutesSource;
40 import com.example.androidx.mediarouting.activities.systemrouting.source.MediaRouter2SystemRoutesSource;
41 import com.example.androidx.mediarouting.activities.systemrouting.source.MediaRouterSystemRoutesSource;
42 import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource;
43 
44 import org.jspecify.annotations.NonNull;
45 import org.jspecify.annotations.Nullable;
46 
47 import java.util.ArrayList;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 
52 /**
53  * Shows available system routes gathered from different sources.
54  */
55 public final class SystemRoutingActivity extends AppCompatActivity {
56 
57     private static final int REQUEST_CODE_BLUETOOTH_CONNECT = 4199;
58 
59     private final @NonNull SystemRoutesAdapter mSystemRoutesAdapter =
60             new SystemRoutesAdapter(this::onRouteItemClicked);
61 
62     private final @NonNull Map<String, SystemRoutesSource> mSystemRoutesSources = new HashMap<>();
63     private @NonNull SwipeRefreshLayout mSwipeRefreshLayout;
64 
65     /**
66      * Creates and launches an intent to start current activity.
67      */
launch(@onNull Context context)68     public static void launch(@NonNull Context context) {
69         Intent intent = new Intent(context, SystemRoutingActivity.class);
70         context.startActivity(intent);
71     }
72 
73     @Override
onCreate(@ullable Bundle savedInstanceState)74     protected void onCreate(@Nullable Bundle savedInstanceState) {
75         super.onCreate(savedInstanceState);
76         setContentView(R.layout.activity_system_routing);
77 
78         RecyclerView recyclerView = findViewById(R.id.recycler_view);
79         mSwipeRefreshLayout = findViewById(R.id.pull_to_refresh_layout);
80 
81         recyclerView.setAdapter(mSystemRoutesAdapter);
82         recyclerView.setLayoutManager(new LinearLayoutManager(this));
83 
84         mSwipeRefreshLayout.setOnRefreshListener(this::refreshSystemRoutesList);
85 
86         if (hasBluetoothPermission()) {
87             initializeSystemRoutesSources();
88             refreshSystemRoutesList();
89         } else {
90             requestBluetoothPermission();
91         }
92     }
93 
94     @Override
onDestroy()95     protected void onDestroy() {
96         for (SystemRoutesSource source : mSystemRoutesSources.values()) {
97             source.stop();
98         }
99 
100         super.onDestroy();
101     }
102 
103     @Override
onRequestPermissionsResult(int requestCode, String @NonNull [] permissions, int @NonNull [] grantResults)104     public void onRequestPermissionsResult(int requestCode, String @NonNull [] permissions,
105             int @NonNull [] grantResults) {
106         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
107 
108         if (requestCode == REQUEST_CODE_BLUETOOTH_CONNECT
109                 && grantResults.length > 0) {
110             if (grantResults[0] == PERMISSION_GRANTED) {
111                 onBluetoothPermissionGranted();
112             } else {
113                 onBluetoothPermissionDenied();
114             }
115         }
116     }
117 
refreshSystemRoutesList()118     private void refreshSystemRoutesList() {
119         List<SystemRoutesAdapterItem> systemRoutesSourceItems = new ArrayList<>();
120         for (SystemRoutesSource source : mSystemRoutesSources.values()) {
121             systemRoutesSourceItems.add(source.getSourceItem());
122             systemRoutesSourceItems.addAll(source.fetchSourceRouteItems());
123         }
124         mSystemRoutesAdapter.setItems(systemRoutesSourceItems);
125         mSwipeRefreshLayout.setRefreshing(false);
126     }
127 
onRouteItemClicked(SystemRouteItem item)128     private void onRouteItemClicked(SystemRouteItem item) {
129         SystemRoutesSource systemRoutesSource = mSystemRoutesSources.get(item.mSourceId);
130         if (systemRoutesSource == null) {
131             throw new IllegalStateException("Couldn't find source with id: " + item.mSourceId);
132         }
133         if (!systemRoutesSource.select(item)) {
134             Toast.makeText(
135                             /* context= */ this,
136                             "Something went wrong with route selection",
137                             Toast.LENGTH_LONG)
138                     .show();
139         }
140     }
141 
hasBluetoothPermission()142     private boolean hasBluetoothPermission() {
143         return ContextCompat.checkSelfPermission(/* context= */ this, BLUETOOTH_CONNECT)
144                 == PERMISSION_GRANTED
145                 && ContextCompat.checkSelfPermission(/* context= */ this, BLUETOOTH_SCAN)
146                 == PERMISSION_GRANTED;
147     }
148 
requestBluetoothPermission()149     private void requestBluetoothPermission() {
150         ActivityCompat.requestPermissions(this,
151                 new String[]{BLUETOOTH_CONNECT, BLUETOOTH_SCAN}, REQUEST_CODE_BLUETOOTH_CONNECT);
152     }
153 
onBluetoothPermissionGranted()154     private void onBluetoothPermissionGranted() {
155         initializeSystemRoutesSources();
156         refreshSystemRoutesList();
157     }
158 
onBluetoothPermissionDenied()159     private void onBluetoothPermissionDenied() {
160         Toast.makeText(this, getString(R.string.system_routing_activity_bluetooth_denied),
161                 Toast.LENGTH_LONG).show();
162     }
163 
initializeSystemRoutesSources()164     private void initializeSystemRoutesSources() {
165         ArrayList<SystemRoutesSource> sources = new ArrayList<>();
166         sources.add(MediaRouterSystemRoutesSource.create(/* context= */ this));
167 
168         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
169             sources.add(MediaRouter2SystemRoutesSource.create(/* context= */ this));
170         }
171 
172         sources.add(AndroidXMediaRouterSystemRoutesSource.create(/* context= */ this));
173 
174         if (hasBluetoothPermission()) {
175             sources.add(BluetoothManagerSystemRoutesSource.create(/* context= */ this));
176         }
177 
178         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
179             sources.add(AudioManagerSystemRoutesSource.create(/* context= */ this));
180         }
181 
182         mSystemRoutesSources.clear();
183         for (SystemRoutesSource source : sources) {
184             source.setOnRoutesChangedListener(this::refreshSystemRoutesList);
185             mSystemRoutesSources.put(source.getSourceId(), source);
186             source.start();
187         }
188     }
189 }
190