1/*
2 * Copyright (c) 2016, 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 */
16package com.android.car.overview;
17
18import android.app.Activity;
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.graphics.Canvas;
24import android.graphics.Rect;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.IBinder;
28import android.os.RemoteException;
29import android.support.v7.widget.RecyclerView;
30import android.util.Log;
31import android.view.Gravity;
32import android.view.View;
33import android.widget.FrameLayout;
34import android.widget.Toast;
35import com.android.car.stream.IStreamConsumer;
36import com.android.car.stream.IStreamService;
37import com.android.car.stream.StreamCard;
38import com.android.car.stream.StreamConstants;
39import com.android.car.view.PagedListView;
40
41import java.util.List;
42
43/**
44 * An overview activity that presents {@link StreamCard} as scrollable list.
45 */
46public class StreamOverviewActivity extends Activity {
47    private static final String TAG = "Overview";
48    private static final int SERVICE_CONNECTION_RETRY_DELAY_MS = 5000;
49    private static final String ACTION_CAR_OVERVIEW_STATE_CHANGE
50            = "android.intent.action.CAR_OVERVIEW_APP_STATE_CHANGE";
51    private static final String EXTRA_CAR_OVERVIEW_FOREGROUND
52            = "android.intent.action.CAR_APP_STATE";
53
54    private static final int PERMISSION_ACTIVITY_REQUEST_CODE = 5151;
55
56    private PagedListView mPageListView;
57    private View mPermissionText;
58    private StreamAdapter mAdapter;
59    private final Handler mHandler = new Handler();
60    private int mConnectionRetryCount;
61
62    private IStreamService mService;
63    private StreamServiceConnection mConnection;
64
65    private int mCardBottomMargin;
66    private Toast mToast;
67
68    boolean mCheckPermissionsOnResume;
69
70    @Override
71    protected void onCreate(Bundle savedInstanceState) {
72        super.onCreate(savedInstanceState);
73        setContentView(R.layout.overview_activity);
74        mCardBottomMargin = getResources().getDimensionPixelSize(R.dimen.stream_card_bottom_margin);
75
76        int statusBarHeight = getStatusBarHeight();
77
78        FrameLayout.LayoutParams params
79                = (FrameLayout.LayoutParams) findViewById(R.id.action_icon_bar).getLayoutParams();
80        params.setMargins(0, statusBarHeight, 0, 0);
81
82        mAdapter = new StreamAdapter(this /* context */);
83
84        mPageListView = (PagedListView) findViewById(R.id.list_view);
85        mPageListView.setAdapter(mAdapter);
86        mPageListView.addItemDecoration(new DefaultDecoration());
87        mPageListView.setLightMode();
88
89        int listTopMargin = statusBarHeight
90                + getResources().getDimensionPixelSize(R.dimen.lens_header_height);
91        FrameLayout.LayoutParams listViewParams
92                = (FrameLayout.LayoutParams) mPageListView.getLayoutParams();
93        listViewParams.setMargins(0, listTopMargin, 0, 0);
94
95        mPermissionText = findViewById(R.id.permission_text);
96        mPermissionText.setOnClickListener(new View.OnClickListener() {
97            @Override
98            public void onClick(View v) {
99                startPermissionsActivity(false /* checkPermissionsOnly */);
100            }
101        });
102
103        mToast = Toast.makeText(StreamOverviewActivity.this,
104                getString(R.string.voice_assistant_help_msg), Toast.LENGTH_SHORT);
105        mToast.setGravity(Gravity.CENTER, 0, 0);
106        findViewById(R.id.voice_button).setOnClickListener(new View.OnClickListener() {
107            @Override
108            public void onClick(View v) {
109                mToast.show();
110            }
111        });
112
113        findViewById(R.id.gear_button).setOnClickListener(new View.OnClickListener() {
114            @Override
115            public void onClick(View v) {
116                startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS));
117            }
118        });
119
120        startPermissionsActivity(true /* checkPermissionsOnly */);
121    }
122
123    private void startPermissionsActivity(boolean checkPermissionsOnly) {
124        // Start StreamService's permission activity before binding to it.
125        Intent intent = new Intent();
126        intent.setComponent(new ComponentName(
127                getString(R.string.car_stream_item_manager_package_name),
128                getString(R.string.car_stream_item_manager_permissions_activity)));
129        intent.putExtra(StreamConstants.STREAM_PERMISSION_CHECK_PERMISSIONS_ONLY,
130                checkPermissionsOnly);
131        startActivityForResult(intent, PERMISSION_ACTIVITY_REQUEST_CODE);
132    }
133
134    @Override
135    protected void onResume() {
136        super.onResume();
137        if (mCheckPermissionsOnResume) {
138            startPermissionsActivity(true /* checkPermissionsOnly */);
139        } else {
140            mCheckPermissionsOnResume = true;
141        }
142    }
143
144    @Override
145    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
146        if (requestCode == PERMISSION_ACTIVITY_REQUEST_CODE) {
147            // onResume is called after onActivityResult, if the permissions activity has
148            // already finished, then don't bother checking for permissions again on resume.
149            mCheckPermissionsOnResume = false;
150            if (resultCode == Activity.RESULT_OK) {
151                mPermissionText.setVisibility(View.GONE);
152                mPageListView.setVisibility(View.VISIBLE);
153                bindStreamService();
154            } else {
155                mPermissionText.setVisibility(View.VISIBLE);
156                mPageListView.setVisibility(View.GONE);
157            }
158        }
159    }
160
161    @Override
162    protected void onDestroy() {
163        // Do a tear down to avoid leaks
164        mHandler.removeCallbacks(mServiceConnectionRetry);
165
166        if (mConnection != null) {
167            unbindService(mConnection);
168        }
169        super.onDestroy();
170    }
171
172    @Override
173    protected void onStart() {
174        super.onStart();
175        Intent i = new Intent(ACTION_CAR_OVERVIEW_STATE_CHANGE);
176        i.putExtra(EXTRA_CAR_OVERVIEW_FOREGROUND, true);
177        sendBroadcast(i);
178    }
179
180    @Override
181    protected void onStop() {
182        Intent i = new Intent(ACTION_CAR_OVERVIEW_STATE_CHANGE);
183        i.putExtra(EXTRA_CAR_OVERVIEW_FOREGROUND, false);
184        sendBroadcast(i);
185        super.onStop();
186    }
187
188    private class StreamServiceConnection implements ServiceConnection {
189        @Override
190        public void onServiceConnected(ComponentName name, IBinder service) {
191            mConnectionRetryCount = 1;
192            // If there is currently a retry scheduled, cancel it.
193            if (mServiceConnectionRetry != null) {
194                mHandler.removeCallbacks(mServiceConnectionRetry);
195            }
196
197            mService = IStreamService.Stub.asInterface(service);
198            if (Log.isLoggable(TAG, Log.DEBUG)) {
199                Log.d(TAG, "Service connected");
200            }
201            try {
202                mService.registerConsumer(mStreamConsumer);
203
204                List<StreamCard> cards = mService.fetchAllStreamCards();
205                if (cards != null) {
206                    for (StreamCard card : cards) {
207                        mAdapter.addCard(card);
208                    }
209                }
210
211
212            } catch (RemoteException e) {
213                throw new IllegalStateException("not connected to IStreamItemManagerService");
214            }
215        }
216
217        @Override
218        public void onServiceDisconnected(ComponentName name) {
219            mService = null;
220            mAdapter.removeAllCards();
221            if (Log.isLoggable(TAG, Log.DEBUG)) {
222                Log.d(TAG, "Service disconnected, reconnecting...");
223            }
224
225            mHandler.removeCallbacks(mServiceConnectionRetry);
226            mHandler.postDelayed(mServiceConnectionRetry, SERVICE_CONNECTION_RETRY_DELAY_MS);
227        }
228    }
229
230    private Runnable mServiceConnectionRetry = new Runnable() {
231        @Override
232        public void run() {
233            if (mService != null) {
234                if (Log.isLoggable(TAG, Log.DEBUG)) {
235                    Log.d(TAG, "Stream service rebound by framework, no need to bind again");
236                }
237                return;
238            }
239            mConnectionRetryCount++;
240            if (Log.isLoggable(TAG, Log.DEBUG)) {
241                Log.d(TAG, "Rebinding disconnected Stream Service, retry count: "
242                        + mConnectionRetryCount);
243            }
244
245            if (!bindStreamService()) {
246                mHandler.postDelayed(mServiceConnectionRetry,
247                        mConnectionRetryCount * SERVICE_CONNECTION_RETRY_DELAY_MS);
248            }
249        }
250    };
251
252    private final IStreamConsumer mStreamConsumer = new IStreamConsumer.Stub() {
253        @Override
254        public void onStreamCardAdded(StreamCard card) throws RemoteException {
255            StreamOverviewActivity.this.runOnUiThread(new Runnable() {
256                public void run() {
257                    if (Log.isLoggable(TAG, Log.DEBUG)) {
258                        Log.d(TAG, "Stream Card added: " + card);
259                    }
260                    mAdapter.addCard(card);
261                }
262            });
263        }
264
265        @Override
266        public void onStreamCardRemoved(StreamCard card) throws RemoteException {
267            StreamOverviewActivity.this.runOnUiThread(new Runnable() {
268                public void run() {
269                    if (Log.isLoggable(TAG, Log.DEBUG)) {
270                        Log.d(TAG, "Stream Card removed: " + card);
271                    }
272                    mAdapter.removeCard(card);
273                }
274            });
275        }
276
277        @Override
278        public void onStreamCardChanged(StreamCard newStreamCard) throws RemoteException {
279        }
280    };
281
282    private boolean bindStreamService() {
283        mConnection = new StreamServiceConnection();
284        Intent intent = new Intent();
285        intent.setAction(StreamConstants.STREAM_CONSUMER_BIND_ACTION);
286        intent.setComponent(new ComponentName(
287                getString(R.string.car_stream_item_manager_package_name),
288                getString(R.string.car_stream_item_manager_class_name)));
289
290        boolean bound = bindService(intent, mConnection, BIND_AUTO_CREATE);
291        if (Log.isLoggable(TAG, Log.DEBUG)) {
292            Log.d(TAG, "IStreamItemManagerService bound: " + bound
293                    + "; component: " + intent.getComponent());
294        }
295
296        return bound;
297    }
298
299    private class DefaultDecoration extends RecyclerView.ItemDecoration {
300        @Override
301        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
302                RecyclerView.State state) {
303            outRect.bottom = mCardBottomMargin;
304        }
305    }
306
307    private int getStatusBarHeight() {
308        int result = 0;
309        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
310        if (resourceId > 0) {
311            result = getResources().getDimensionPixelSize(resourceId);
312        }
313        return result;
314    }
315}
316